名前空間
変種
操作

仮想関数指定子

提供: cppreference.com
< cpp‎ | language
 
 
C++言語
一般的なトピック
フロー制御
条件付き実行文
繰り返し文 (ループ)
ジャンプ文
関数
関数宣言
ラムダ関数宣言
inline 指定子
例外指定 (C++20未満)
noexcept 指定子 (C++11)
例外
名前空間
指定子
decltype (C++11)
auto (C++11)
alignas (C++11)
記憶域期間指定子
初期化
代替表現
リテラル
ブーリアン - 整数 - 浮動小数点
文字 - 文字列 - nullptr (C++11)
ユーザ定義 (C++11)
ユーティリティ
属性 (C++11)
typedef 宣言
型エイリアス宣言 (C++11)
キャスト
暗黙の変換 - 明示的な変換
static_cast - dynamic_cast
const_cast - reinterpret_cast
メモリ確保
クラス
クラス固有の関数特性
仮想関数
override 指定子 (C++11)
final 指定子 (C++11)
特別なメンバ関数
テンプレート
その他
 
 

virtual 指定子は非静的メンバ関数仮想であり動的ディスパッチをサポートすることを指定します。 非静的メンバ関数の最初の宣言 (すなわち、クラス定義で宣言されるとき) の decl-specifier-seq にのみ現れることができます。

目次

[編集] 説明

仮想関数は動作が派生クラスでオーバーライドできるメンバ関数です。 非仮想関数と異なり、オーバーライドされた動作は、たとえクラスの実際の型に関するコンパイル時情報がなくても、維持されます。 派生クラスが基底クラスへのポインタまたは参照を用いて処理される場合、オーバーライドされた仮想関数の呼び出しは派生クラスで定義された動作を呼び出します。 この動作は関数が修飾名の名前探索を用いて選択された場合 (つまり、関数の名前がスコープ解決演算子 :: の右に現れた場合) は抑制されます。

#include <iostream>
struct Base {
   virtual void f() {
       std::cout << "base\n";
   }
};
struct Derived : Base {
    void f() override { // 「override」はオプショナルです。
        std::cout << "derived\n";
    }
};
int main()
{
    Base b;
    Derived d;
 
    // 参照を通した仮想関数の呼び出し。
    Base& br = b; // br の型は Base& です。
    Base& dr = d; // dr の型も同様に Base& です。
    br.f(); // 「base」を表示します。
    dr.f(); // 「derived」を表示します。
 
    // ポインタを通した仮想関数の呼び出し。
    Base* bp = &b; // bp の型は Base* です。
    Base* dp = &d; // dp の型も同様に Base* です。
    bp->f(); // 「base」を表示します。
    dp->f(); // 「derived」を表示します。
 
    // 非仮想関数の呼び出し。
    br.Base::f(); // 「base」を表示します。
    dr.Base::f(); // 「base」を表示します。
}


[編集] 詳細

何らかのメンバ関数 vf がクラス Basevirtual として宣言され、 Base から直接または間接的に派生した何らかのクラス Derived が同じ

  • 名前
  • 引数の型のリスト (戻り値は含みません)
  • cv 修飾子
  • 参照修飾子

のメンバ関数の宣言を持つ場合、クラス Derived のその関数も仮想であり (その宣言でキーワード virtual が使用されたか否かにかかわらず)、 Base::vf をオーバーライドします (その宣言で単語 override が使用されたか否かにかかわらず)。

Base::vf はオーバーライドされるために可視である必要はありません (private 宣言したり private 継承で継承していても構いません)。

class B {
    virtual void do_f(); // プライベートメンバ。
 public:
    void f() { do_f(); } // パブリック継承。
};
struct D : public B {
    void do_f() override; // B::do_f をオーバーライドします。
};
 
int main()
{
    D d;
    B* bp = &d;
    bp->f(); // (間接的に) D::do_f() を呼びます。
}

すべての仮想関数について、仮想関数呼び出しが行われたときに実行される最終オーバーライダーが存在します。 派生クラスが vf をオーバーライドする別の関数を宣言または (多重継承を通して) 継承しなければ、基底クラス base の仮想メンバ関数 vf が最終オーバーライダーです。

struct A { virtual void f(); };     // A::f は仮想です。
struct B : A { void f(); };         // B::f は A::f を B でオーバーライドします。
struct C : virtual B { void f(); }; // C::f は A::f を C でオーバーライドします。
struct D : virtual B {}; // D はオーバーライダーを導入しません。 B::f が D の最終です。
struct E : C, D  {       // E はオーバーライダーを導入しません。 C::f が E の最終です。
    using A::f; // 関数宣言ではありません。 名前探索で A::f を可視にするだけです。
};
int main() {
   E e;
   e.f();    // 仮想関数呼び出しは e における最終オーバーライダー C::f を呼びます。
   e.E::f(); // 非仮想呼び出しは E で可視な A::f を呼びます。
}

関数が複数の最終オーバーライダーを持つ場合、プログラムは ill-formed です。

struct A {
    virtual void f();
};
struct VB1 : virtual A {
    void f(); // A::f をオーバーライドします。
};
struct VB2 : virtual A {
    void f(); // A::f をオーバーライドします。
};
// struct Error : VB1, VB2 {
//     // エラー、 A::f は2つの最終オーバーライダーを持ちます。
// };
struct Okay : VB1, VB2 {
    void f(); // OK、これが A::f に対する最終オーバーライダーになります。
};
struct VB1a : virtual A {}; // オーバーライダーを宣言しません。
struct Da : VB1a, VB2 {
    // OK、 Da における A::f の最終オーバーライダーは VB2::f です。
};

名前が同じだけれども引数リストが異なる関数は同じ名前の基底の関数を、オーバーライドしませんが、隠蔽します。 非修飾名の名前探索が派生クラスのスコープを調査するとき、名前探索はその宣言を発見し、基底クラスを調査しません。

struct B {
    virtual void f();
};
struct D : B {
    void f(int); // D::f は B::f を隠蔽します (引数リストが違っています)。
};
struct D2 : D {
    void f(); // D2::f は B::f をオーバーライドします (可視でないことは問題になりません)。
};
 
int main()
{
    B b;   B& b_as_b   = b;
    D d;   B& d_as_b   = d;    D& d_as_d = d;
    D2 d2; B& d2_as_b  = d2;   D& d2_as_d = d2;
 
    b_as_b.f(); // B::f() を呼びます。
    d_as_b.f(); // B::f() を呼びます。
    d2_as_b.f(); // D2::f() を呼びます。
 
    d_as_d.f(); // エラー、 D における名前探索は f(int) のみを発見します。
    d2_as_d.f(); // エラー、 D における名前探索は f(int) のみを発見します。
}

関数が override 指定子付きで宣言されたけれども仮想関数をオーバーライドしない場合、プログラムは ill-formed です。

struct B {
    virtual void f(int);
};
struct D : B {
    virtual void f(int) override; // OK、 D::f(int) は B::f(int) をオーバーライドします。
    virtual void f(long) override; // エラー、 f(long) は B::f(int) をオーバーライドしません。
};

関数が final 指定子付きで宣言され、別の関数がそのオーバーライドを試みた場合、プログラムは ill-formed です。

struct B {
    virtual void f() const final;
};
struct D : B {
    void f() const; // エラー、 D::f が final である B::f のオーバーライドを試みました。
};
(C++11以上)

非メンバ関数および静的メンバ関数は仮想にできません。

関数テンプレートは virtual 宣言できません。 これはそれ自身がテンプレートである関数にのみ適用されます。 クラステンプレートの普通のメンバ関数は virtual 宣言できます。

仮想関数 (virtual 宣言されたかオーバーライドかにかかわらず) はいかなる紐付いた制約も持つことができません。

struct A {
    virtual void f() requires true; // エラー、制約付き仮想関数。
};

consteval 仮想関数は非 consteval 仮想関数をオーバーライドするまたは非 consteval によってオーバーライドされることはできません。

(C++20以上)

仮想関数のデフォルト引数はコンパイル時に置き換えられます。

[編集] 共変戻り値型

関数 Derived::f が関数 Base::f をオーバーライドする場合、その戻り値の型は同じか共変のいずれかでなければなりません。 以下の要件をすべて満たす場合、その2つの型は共変です。

  • 両方の型がクラスへのポインタまたは参照 (の左辺値または右辺値) である。 多段ポインタまたは多段参照は許容されません。
  • Base::f() の戻り値の型の参照先/指す先のクラスは、 Derived::f() の戻り値の型の参照先/指す先のクラスの非曖昧かつアクセス可能な直接または間接的な基底クラスでなければなりません。
  • Derived::f() の戻り値の型は Base::f() の戻り値の型と等しくまたはより少なく cv 修飾されていなければなりません。

Derived::f の戻り値の型内のクラスは、 Derived 自身であるか、 Derived::f の宣言の地点において完全型でなければなりません。

���想関数呼び出しが行われたとき、最終オーバーライダーによって返された型は、呼ばれたオーバーライドされた関数の戻り値の型に暗黙に変換されます。

class B {};
 
struct Base {
    virtual void vf1();
    virtual void vf2();
    virtual void vf3();
    virtual B* vf4();
    virtual B* vf5();
};
 
class D : private B {
    friend struct Derived; // Derived において、 B は D のアクセス可能な基底です。
};
 
class A; // 前方宣言されたクラスは不完全型です。
 
struct Derived : public Base {
    void vf1();    // 仮想。 Base::vf1() をオーバーライドします。
    void vf2(int); // 非仮想。 Base::vf2() を隠蔽します。
//  char vf3();    // エラー、 Base::vf3 をオーバーライドしますが、
                   // 異なった共変でない戻り値の型を持ちます。
    D* vf4();      // Base::vf4() をオーバーライドし、共変戻り値型を持ちます。
//  A* vf5();      // エラー、 A は不完全型です。
};
 
int main()
{
    Derived d;
    Base& br = d;
    Derived& dr = d;
 
    br.vf1(); // Derived::vf1() を呼びます。
    br.vf2(); // Base::vf2() を呼びます。
//  dr.vf2(); // エラー、 vf2(int) が vf2() を隠蔽します。
 
    B* p = br.vf4(); // Derived::vf4() を呼び、その結果を B* に変換します。
    D* q = dr.vf4(); // Derived::vf4() を呼び、その結果を B* に変換しません。
 
}

[編集] 仮想デストラクタ

たとえデストラクタが継承されなくとも、基底クラスがそのデストラクタを virtual 宣言した場合、その派生のデストラクタは常にそれをオーバーライドします。 これは動的に確保された多相型のオブジェクトの基底へのポインタを通した削除を可能とします。

class Base {
 public:
    virtual ~Base() { /* Base のリソースを解放します。 */ }
};
 
class Derived : public Base {
    ~Derived() { /* Derived のリソースを解放します。 */ }
};
 
int main()
{
    Base* b = new Derived;
    delete b; // Base::~Base() の仮想関数呼び出しを行います。
              // これは virtual であるため Derived::~Derived() を呼びます。
              // Derived::~Derived() は派生クラスのリソースを解放することができ、
              // その後、通常の破棄順序に従って Base::~Base() を呼びます。
}

また、クラスが多相であり (少なくともひとつの仮想関数を宣言または継承し)、そのデストラクタが仮想でない場合、派生したデストラクタが呼ばれなければ、リークするリソースがあるかどうかにかかわらず、その削除は未定義動作です。

あらゆる基底クラスのデストラクタはパブリックかつ仮想またはプロテクテッドかつ非仮想でなければならない、というのが役に立つガイドラインです。

[編集] 構築および破棄の途中

仮想関数がコンストラクタまたはデストラクタ (クラスの非静的データメンバの構築または破棄の途中 (例えばメンバ初期化子リスト内など) を含みます) から直接または間接的に呼ばれ、その呼び出しの適用先のオブジェクトが構築または破棄の途中のオブジェクトである場合、呼ばれる関数はそのコンストラクタまたはデストラクタのクラスにおける最終オーバーライダーであり、より派生したクラスにおけるそれをオーバーライドしたものではありません。 別の言い方をすると、構築または破棄の途中では、より派生したクラスは存在しません。

複数の分岐を持つ複雑なクラスを構築するとき、ある分岐に属するコンストラクタ内では、多相性はそのクラスとその基底に制限されます。 この部分継承階層の外側の基底部分オブジェクトへの取得または参照を取得し、仮想関数呼び出しを (例えば明示的なメンバアクセスを用いて) 試みた場合、動作は未定義です。

struct V {
    virtual void f();
    virtual void g();
};
 
struct A : virtual V {
    virtual void f(); // A::f は A における V::f の最終オーバーライダーです。
};
struct B : virtual V {
    virtual void g(); // B::g は B における V::g の最終オーバーライダーです。
    B(V*, A*);
};
struct D : A, B {
    virtual void f(); // D::f は D における V::f の最終オーバーライダーです。
    virtual void g(); // D::g は D における V::g の最終オーバーライダーです。
 
    // 注: A は B より前に初期化されます。
    D() : B((A*)this, this) 
    {
    }
};
 
// B のコンストラクタ。 D のコンストラクタから呼ばれます。
B::B(V* v, A* a)
{
    f(); // V::f の仮想呼び出し (D が最終オーバーライダーを持ちますが、 D はまだ存在しません)。
    g(); // B::g の仮想呼び出し (B における最終オーバーライダーです)。
 
    v->g(); // v の型 V は B の基底であり、仮想呼び出しは前と同様に B::g を呼びます。
 
    a->f(); // a の型 A は B の基底ではありません。 これは継承階層の別の分岐に属します。
            // その分岐を通した仮想呼び出しの試みは、たとえこの場合 A がすでに完全に
            // 構築されているとしても (D の基底のリスト内で A は B より前に現れるため、
            // A は B より前に構築されます)、未定義動作を発生させます。
            // 実際のところ、 A::f の仮想呼び出しは B の仮想メンバ関数テーブル
            // (B の構築中はそれがアクティブであるため) を用いて試みられます。
}

[編集] 関連項目