virtual
function specifier
非静的 member function が 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
がクラス
Base
で
virtual
として宣言され、かつ
Base
から直接または間接的に派生したクラス
Derived
が同じ
- 名前
- パラメータ型リスト(ただし戻り値の型は除く)
- CV修飾子
- 参照修飾子
この場合、クラス
Derived
内のこの関数もまた
virtual
(その宣言でキーワード
virtual
が使用されているかどうかに関わらず)であり、
overrides
Base::vf(その宣言で指定子
override
が使用されているかどうかに関わらず)。
Base::vf
はオーバーライドするためにアクセス可能または可視である必要はありません。(
Base::vf
はprivateで宣言されている場合や、
Base
がprivate継承を使用して継承されている場合でも構いません。
Base
を継承する
Derived
の基底クラス内で同じ名前を持つメンバーは、名前検索時に
Base::vf
を隠蔽する可能性がある場合でも、オーバーライドの決定には影響しません。)
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() を呼び出す }
すべての仮想関数に対して、仮想関数呼び出しが行われたときに実行される
final overrider(最終オーバーライド)
が存在します。基底クラス
Base
の仮想メンバ関数
vf
は、派生クラスが
vf
をオーバーライドする別の関数を(多重継承を通じて)宣言または継承しない限り、final overriderとなります。
struct A { virtual void f(); }; // A::f は仮想関数 struct B : A { void f(); }; // B::f は B 内で A::f をオーバーライド struct C : virtual B { void f(); }; // C::f は C 内で A::f をオーバーライド struct D : virtual B {}; // D はオーバーライダを導入せず、B::f は D 内で final struct E : C, D // E はオーバーライダを導入せず、C::f は E 内で final { using A::f; // 関数宣言ではなく、A::f を参照可能にする }; int main() { E e; e.f(); // 仮想呼び出しは e 内の最終オーバーライダである C::f を呼び出す e.E::f(); // 非仮想呼び出しは E 内で参照可能な A::f を呼び出す }
関数が複数の最終オーバーライドを持つ場合、プログラムは不適格となります:
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 が Error 内で2つの最終オーバーライダを持つ // }; struct Okay : VB1, VB2 { void f(); // OK: これは A::f の最終オーバーライダ }; struct VB1a : virtual A {}; // オーバーライダを宣言しない struct Da : VB1a, VB2 { // Da では、A::f の最終オーバーライダは VB2::f };
同じ名前で異なるパラメータリストを持つ関数は、同じ名前の基底関数をオーバーライドせず、 隠蔽 します: unqualified name lookup が派生クラスのスコープを調べる際、ルックアップは宣言を見つけ、基底クラスを調べません。
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)のみが見つかる }
|
関数が
struct B { virtual void f(int); }; struct D : B { virtual void f(int) override; // OK, D::f(int) overrides B::f(int) virtual void f(long) override; // Error: f(long) does not override B::f(int) };
関数が
struct B { virtual void f() const final; }; struct D : B { void f() const; // Error: D::f attempts to override final B::f }; |
(C++11以降) |
非メンバ関数と静的メンバ関数は仮想関数にできません。
関数テンプレートは
virtual
として宣言できません。これはテンプレート自体である関数にのみ適用されます - クラステンプレートの通常のメンバ関数はvirtualとして宣言できます。
|
仮想関数(virtualで宣言されたもの、または仮想関数をオーバーライドするもの)は関連する制約を持つことはできません。 struct A { virtual void f() requires true; // Error: constrained virtual function };
|
(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(); // virtual、Base::vf1()をオーバーライド void vf2(int); // non-virtual、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()への仮想関数呼び出しを行う // 仮想関数であるため、Derived::~Derived()を呼び出し、 // 派生クラスのリソースを解放した後、通常の破棄順序に従って // Base::~Base()を呼び出す }
さらに、基底クラスのデストラクタが仮想でない場合、派生クラスオブジェクトを基底クラスへのポインタを通じて削除することは、 未定義動作 となります。これは、派生デストラクタが呼び出されない場合にリークするリソースが存在するかどうかに関わらず 、選択された解放関数が破棄 operator delete でない限り (C++20以降) です。
有用なガイドラインとして、基底クラスのデストラクタは、delete式が関与する場合には常に publicかつvirtual、またはprotectedかつnon-virtual でなければならない 、例えば暗黙的に使用される場合 std::unique_ptr (C++11以降) 。
構築および破壊中
仮想関数がコンストラクタまたはデストラクタから直接または間接的に呼び出される場合(クラスの非静的データメンバーの構築または破棄中を含む。例えばメンバー initializer list において)、そして呼び出しが適用されるオブジェクトが構築中または破棄中のオブジェクトである場合、呼び出される関数はコンストラクタまたはデストラクタのクラスにおけるfinal overriderであり、より派生したクラスでそれをオーバーライドする関数ではない。 言い換えれば、構築または破棄中には、より派生したクラスは存在しない。
複数の分岐を持つ複雑なクラスを構築する際に、ある分岐に属するコンストラクタ内では、ポリモーフィズムはそのクラスとその基底クラスに限定されます:このサブ階層外の基底サブオブジェクトへのポインタまたは参照を取得し、仮想関数呼び出し(例えば明示的なメンバアクセスを使用して)を実行しようとすると、動作は未定義となります:
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) {} }; // D のコンストラクタから呼び出される B のコンストラクタ 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 が既に完全に構築されていたとしても //(B より前に構築された、D の基底クラスリストで B より前に現れるため)、未定義動作を引き起こす。 // 実際には、A::f への仮想呼び出しは B の仮想メンバ関数テーブルを使用して試行される //(B の構築中にアクティブなのはそれだからである) }
キーワード
不具合報告
以下の動作変更の欠陥報告書は、以前に公開されたC++規格に対して遡及的に適用されました。
| DR | 適用対象 | 公開時の動作 | 正しい動作 |
|---|---|---|---|
| CWG 258 | C++98 |
派生クラスの非constメンバー関数が、基底クラスのconst仮想メンバー関数によって
仮想関数になる可能性があった |
仮想性にはCV修飾子も
同じであることが必要 |
| CWG 477 | C++98 | friend宣言に virtual 指定子を含めることができた | 許可されない |
| CWG 1516 | C++98 |
「仮想関数呼び出し」と「仮想呼び出し」の用語定義が
提供されていなかった |
提供された |
関連項目
| 派生クラスと継承モード | |
override
指定子
(C++11)
|
メソッドが他のメソッドをオーバーライドすることを明示的に宣言する |
final
指定子
(C++11)
|
メソッドがオーバーライドできないこと、またはクラスが派生できないことを宣言する |