3

C ++のより詳細な継承メカニズムを分析しようとしているときに、次の例に出くわしました。

#include<iostream>

using namespace std;

class Base {
public:
    virtual void f(){
    cout << "Base.f" << endl; 
    }
};

class Left : public virtual Base {    
};

class Right : public virtual Base{
public:
    virtual void f(){
        cout << "Right.f" << endl; 
    }
};

class Bottom : public Left, public Right{

};

int main(int argc,char **argv)
{
    Bottom* b = new Bottom();
    b->f();
}

上記は、どういうわけか、Right :: f()をコンパイルして呼び出します。コンパイラで何が起こっているのか、共有Baseオブジェクトが1つあること、Rightがf()をオーバーライドすることを理解していることがわかりますが、実際には、Left::f()(から継承されたBase::f())とRight::f()の2つのメソッドが必要です。オーバーライドしますBase::f()。さて、Bottomによって継承されている2つの別々のメソッドがあり、どちらも同じシグネチャを持っていることに基づいて、衝突があるはずだと思います。

C ++のどの仕様の詳細がこのケースを扱っているのか、そしてそれが低レベルの観点からどのように行われているのかを誰かが説明できますか?

4

1 に答える 1

4

恐ろしいダイアモンドには単一のベースがあり、そこから2つの中間オブジェクトが派生し、4番目のタイプは中間レベルの両方のタイプからの多重継承でダイアモンドを閉じます。

あなたの質問は、前の例でいくつの関数が宣言されているかということのようです。f答えは1つです。

ベースと派生の線形階層のより簡単な例から始めましょう。

struct base {
   virtual void f() {}
};
struct derived : base {
   virtual void f() {}
};

この例では、f2つのオーバーライドがある宣言された単一のものがbase::fありますderived::f。タイプのオブジェクトではderived、最終的なオーバーライドはderived::fです。f両方の関数が、複数の実装を持つ単一の関数を表すことに注意することが重要です。

さて、元の例に戻って、右側の行でBase::fRight::f同じようにオーバーライドされたのと同じ関数です。したがって、タイプがのオブジェクトのRight場合、最終的なオーバーライドはRight::fです。これで、タイプのfinalオブジェクトのLeft場合、finalオーバーライドは関数をオーバーライドBase::fLeftないようになります。

ひし形が閉じているとき、および継承がvirtual存在するため、単一のBaseオブジェクトが存在するため、単一の関数を宣言しfます。継承の第2レベルでは、Rightその関数を独自の実装でオーバーライドします。これは、最も派生した型の最後のオーバーライドですBottom

これを標準の外で見て、これが実際にコンパイラによってどのように実装されているかを確認することをお勧めします。コンパイラは、オブジェクトを作成するときに、仮想テーブルにBase非表示のポインタを追加します。vptr仮想テーブルはサンクへのポインターを保持します簡単にするために、テーブルが関数の最後のオーバーライドへのポインターを保持していると仮定します[1])。この場合、Baseオブジェクトにはメンバーデータは含まれず、関数へのポインターを保持するテーブルへのポインターのみが含まれますBase::f

Leftextendsを拡張するBaseと、新しいvtableが作成され、そのvtable内のポインターがこのレベルLeftの最後のオーバーライドに設定されます。これは偶然にも両方のvtable内のポインター(トランポリンを無視)が同じ実際の実装にジャンプするためです。タイプのオブジェクトが構築されているとき、サブオブジェクトが最初に初期化され、次に(存在する場合)のメンバーの初期化の前に、参照するようにポインターが更新されます(つまり、に格納されたポインターはに対して定義されたテーブルを参照します)。fBase::fLeftBaseLeftBase::vptrLeft::vtableBaseLeft

ひし形の反対側では、作成されたvtableに、を呼び出すことRightになる単一のサンクRight::fが含まれています。タイプのオブジェクトRightが作成される場合、同じ初期化プロセスが発生し、Base::vptrはを指しDerived::fます。

これで、最終オブジェクトに到達しますBottom。この場合も、タイプに対してvtableが生成され、Bottomそのvtableには、他のすべての場合と同様に、を表す単一のエントリが含まれますf。コンパイラは継承の階層を分析し、Right::fオーバーライドを決定しますBase::f。左側のブランチには同等のオーバーライドがないため、Bottomのvtableでは、を表すポインタはfを参照しRight::fます。Bottomこの場合も、オブジェクトの構築中に、のvtableBase::vptrを参照するようにが更新されます。Bottom

ご覧のとおり、4つのvtableすべてに1つのエントリがfあり、各vtableに格納されている値が異なっていても(最終的なオーバーライドは異なります)、プログラムには1つのエントリがあります。 f

[1]サンクthisは、必要に応じてポインターを適応させ(通常、多重継承が必要であることを意味します)、呼び出しを実際のオーバーライドに転送する小さなコードです。単一継承の場合、thisポインターを更新する必要はなく、サンクは消え、vtableのエントリは実際の関数を直接指します。

于 2012-04-15T23:10:54.713 に答える