web-dev-qa-db-ja.com

派生クラスの関数のC ++「仮想」キーワード。それは必要ですか?

以下に示す構造体定義で...

struct A {
    virtual void hello() = 0;
};

アプローチ#1:

struct B : public A {
    virtual void hello() { ... }
};

アプローチ#2:

struct B : public A {
    void hello() { ... }
};

Hello関数をオーバーライドするこれら2つの方法に違いはありますか?

207
Anarki

それらはまったく同じです。最初のアプローチではより多くのタイピングが必要であり、明確になる可能性があること以外は、両者の間に違いはありません。

167
James McNellis

関数の「仮想性」は暗黙的に伝播されますが、virtualキーワードが明示的に使用されていない場合、使用するコンパイラの少なくとも1つが警告を生成します。

virtualキーワードを含む純粋に文体的な観点から、関数が仮想であることをユーザーに明確に「広告」します。これは、Aの定義をチェックせずにBをさらにサブクラス化する人にとって重要です。深いクラス階層では、これは特に重要になります。

81
Clifford

virtualキーワードは、派生クラスでは必要ありません。これは、C++ Draft Standard(N3337)(強調鉱山)からのサポートドキュメントです。

10.3仮想関数

2仮想メンバー関数vfがクラスBaseおよびクラスDerivedで宣言されている場合、Baseから直接または間接的に派生し、メンバー関数vfBase::vfが宣言されているのと同じ名前、parameter-type-list(8.3.5)、cv-qualification、およびref-qualifier(または同じものがない)で、Derived::vfも仮想(- そのように宣言されているかどうか)そして、Base::vfをオーバーライドします。

49
R Sahu

いいえ、派生クラスの仮想関数オーバーライドのvirtualキーワードは必要ありません。しかし、関連する落とし穴に言及する価値があります。仮想関数をオーバーライドできないことです。

オーバーライドの失敗は、派生クラスの仮想関数をオーバーライドするつもりであるが、署名でエラーを作成して新しいおよび異なる仮想機能。この関数は、基本クラス関数のoverloadであるか、名前が異なる場合があります。派生クラスの関数宣言でvirtualキーワードを使用するかどうかに関係なく、コンパイラは、基本クラスの関数をオーバーライドするつもりであることを認識できません。

ただし、この落とし穴は、C++ 11 明示的なオーバーライド 言語機能によって感謝して対処されています。これにより、ソースコードは、メンバー関数が基本クラス関数をオーバーライドすることを明確に指定できます。

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

コンパイラはコンパイル時エラーを発行し、プログラミングエラーはすぐに明らかになります(おそらく、Derivedの関数はfloatを引数として使用するはずです)。

WP:C++ 11 を参照してください。

31
Colin D Bennett

"virtual"キーワードを追加すると読みやすくなりますが、必須ではありません。基本クラスで仮想と宣言され、派生クラスで同じシグネチャを持つ関数は、デフォルトで「仮想」と見なされます。

11
Sujay Ghosh

派生クラスにvirtualを記述する場合、または省略する場合、コンパイラーに違いはありません。

ただし、この情報を取得するには、基本クラスを調べる必要があります。したがって、この関数が仮想であることを人間に見せたい場合は、派生クラスにもvirtualキーワードを追加することをお勧めします。

7
harper

テンプレートがあり、テンプレートパラメータとして基本クラスを取得し始めると、かなりの違いがあります。

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

それの楽しい部分は、インターフェイスと非インターフェイス関数を定義できるようになったことです後でクラスを定義します。これは、ライブラリ間のインターワーキングインターフェイスに役立ちます(single libraryの標準設計プロセスとしてこれに依存しないでください)。すべてのクラスでこれを許可しても、費用はかかりません。必要に応じてtypedef Bに変更することもできます。

これを行う場合、コピー/移動コンストラクターをテンプレートとして宣言することもできます。異なるインターフェイスから構築できるようにすることで、異なるB<>タイプ間で「キャスト」できます。

t_hello()const A&のサポートを追加する必要があるかどうかは疑問です。この書き換えの通常の理由は、主にパフォーマンス上の理由から、継承ベースの特殊化からテンプレートベースの特殊化に移行することです。古いインターフェイスを引き続きサポートする場合、古い使用を検出(または抑止)することはできません。

1
lorro

virtualキーワードを基本クラスの関数に追加して、オーバーライド可能にする必要があります。この例では、struct Aが基本クラスです。 virtualは、派生クラスでこれらの関数を使用しても意味がありません。ただし、派生クラスも基本クラス自体であり、その関数をオーバーライドできるようにする場合は、virtualをそこに配置する必要があります。

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

ここでCBを継承するため、Bは基本クラスではなく(派生クラスでもあります)、Cは派生クラスです。継承図は次のようになります。

A
^
|
B
^
|
C

そのため、子を持つ可能性のある基本クラス内の関数の前にvirtualを配置する必要があります。 virtualを使用すると、子供が関数をオーバーライドできます。 virtualを派生クラス内の関数の前に置くことには何の問題もありませんが、必須ではありません。ただし、派生クラスから継承したい人がいる場合、メソッドのオーバーライドが期待どおりに機能しないことを喜んでいないため、推奨されます。

したがって、基本クラスの関数をオーバーライドする必要のある子がクラスにないことが確実でない限り、virtualを継承に関係するすべてのクラスの関数の前に置きます。それは良い習慣です。

1
Galaxy

確かに、子クラスのVirtualキーワードを含めます。なぜなら、

  • 私。読みやすさ。
  • ii。この子クラスはさらに派生する可能性があり、さらに派生したクラスのコンストラクターがこの仮想関数を呼び出すことは望ましくありません。
0
user2264698