25

基本クラスから通常の関数メンバーを呼び出すと、C ++で仮想継承を使用すると、コンパイルされたコードで実行時のペナルティが発生しますか?サンプルコード:

class A {
    public:
        void foo(void) {}
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

// ...

D bar;
bar.foo ();
4

7 に答える 7

21

はい、ポインタまたは参照を介してメンバー関数を呼び出し、コンパイラがそのポインタまたは参照が指している、または参照しているオブジェクトのタイプを絶対的に確実に判別できない場合があります。たとえば、次のことを考慮してください。

void f(B* p) { p->foo(); }

void g()
{
    D bar;
    f(&bar);
}

fの呼び出しがインライン化されていないと仮定すると、コンパイラは、Aを呼び出すために仮想基本クラスサブオブジェクトの場所を見つけるためのコードを生成する必要がありますfoo。通常、このルックアップにはvptr/vtableのチェックが含まれます。

ただし、コンパイラが関数を呼び出すオブジェクトのタイプを知っている場合(例の場合のように)、関数呼び出しは静的に(コンパイル時に)ディスパッチできるため、オーバーヘッドは発生しません。あなたの例では、の動的タイプはでbarあることがわかっているのでD(他のものにすることはできません)、仮想基本クラスサブオブジェクトのオフセットはAコンパイル時に計算できます。

于 2011-04-05T14:56:39.320 に答える
14

はい、仮想継承には実行時のパフォーマンスのオーバーヘッドがあります。これは、コンパイラがオブジェクトへのポインタ/参照に対して、コンパイル時にそのサブオブジェクトを見つけることができないためです。対照的に、単一継承の場合、各サブオブジェクトは元のオブジェクトの静的オフセットに配置されます。検討:

class A { ... };
class B : public A { ... }

Bのメモリレイアウトは次のようになります。

| B's stuff | A's stuff |

この場合、コンパイラはAがどこにあるかを知っています。ただし、ここでMVIの場合を考えてみましょう。

class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };

Bのメモリレイアウト:

| B's stuff | A's stuff |

Cのメモリレイアウト:

| C's stuff | A's stuff |

ちょっと待って!Dがインスタンス化されると、そのようには見えません。

| D's stuff | B's stuff | C's stuff | A's stuff |

ここで、B *がある場合、それが実際にBを指している場合、AはB-のすぐ隣にありますが、Dを指している場合、A *を取得するには、Csubをスキップする必要があります。 -オブジェクト。任意の指定B*が実行時に動的にBまたはDを指す可能性があるため、ポインターを動的に変更する必要があります。これは、少なくとも、単一の継承で発生するコンパイル時に値をベイクインするのとは対照的に、何らかの方法でその値を見つけるためのコードを生成する必要があることを意味します。

于 2011-04-05T15:42:33.500 に答える
8

少なくとも一般的な実装では、仮想継承には、データメンバーへの(少なくとも一部の)アクセスに対して(小さな!)ペナルティが伴います。特に、通常、仮想的に派生したオブジェクトのデータメンバーにアクセスするための余分なレベルの間接参照が発生します。これは、(少なくとも通常の場合)2つ以上の別々の派生クラスが同じ基本クラスだけでなく同じ基本クラスオブジェクトを持っているために発生します。これを実現するために、両方の派生クラスには、最も派生したオブジェクトへの同じオフセットへのポインターがあり、そのポインターを介してそれらのデータメンバーにアクセスします。

技術的には仮想継承によるものではありませんが、一般に多重継承には個別の(ここでも小さな)ペナルティがあることに注意する価値があります。単一継承の一般的な実装では、オブジェクトの固定オフセットにvtableポインターがあります(多くの場合、最初の部分です)。多重継承の場合、明らかに同じオフセットに2つのvtableポインターを設定することはできないため、オブジェクト内の別々のオフセットにそれぞれが多数のvtableポインターを配置することになります。

IOW、単一継承のvtableポインターは通常はただstatic_cast<vtable_ptr_t>(object_address)ですが、多重継承では。を取得しますstatic_cast<vtable_ptr_t>(object_address+offset)

技術的には、この2つは完全に分離されていますが、もちろん、仮想継承のほぼ唯一の用途は多重継承との組み合わせであるため、とにかく半関連性があります。

于 2011-04-05T15:49:02.820 に答える
2

具体的には、Microsoft Visual C ++では、メンバーへのポインターのサイズに実際の違いがあります。#pragmapointers_to_membersを参照してください。そのリストからわかるように、最も一般的な方法は「仮想継承」です。これは、多重継承とは異なり、単一継承とは異なります。

これは、仮想継承が存在する場合にメンバーへのポインターを解決するためにより多くの情報が必要であることを意味し、CPUキャッシュに取り込まれるデータの量によってのみ、パフォーマンスに影響を与えます。メンバーのルックアップの長さまたは必要なジャンプの数。

于 2011-04-05T15:00:16.583 に答える
1

仮想継承には実行時のペナルティはないと思います。仮想継承と仮想関数を混同しないでください。どちらも2つの異なるものです。

A仮想継承により、のインスタンスにサブオブジェクトが1つだけ存在することが保証されますDですから、それだけで実行時のペナルティはないと思います。

ただし、コンパイル時にこのサブオブジェクトを認識できない場合が発生する可能性があるため、そのような場合、仮想継承に対して実行時のペナルティが発生します。そのようなケースの1つは、ジェームズの回答で説明されています。

于 2011-04-05T14:57:04.937 に答える
1

あなたの質問は主に仮想ベースの通常の関数を呼び出すことに焦点を当てており、仮想ベースクラス(あなたの例ではクラスA)の仮想関数の(はるかに)興味深いケースではありません-しかし、はい、コストがかかる可能性があります。もちろん、すべてがコンパイラに依存します。

コンパイラがA::fooをコンパイルしたとき、「this」はAのデータメンバーがメモリ内に存在する場所の開始点を指していると想定していました。現時点では、コンパイラはクラスAが他のクラスの仮想ベースになることを認識していない可能性があります。しかし、それは喜んでコードを生成します。

これで、コンパイラがBをコンパイルするときに、実際には変更はありません。Aは仮想基本クラスですが、それでも単一継承であり、通常、コンパイラはクラスAのデータメンバーを直後に配置することでクラスBをレイアウトします。クラスBのデータメンバーによって-したがって、値を変更せずにB*をA*にすぐにキャストできるため、調整を行う必要はありません。コンパイラーは、同じ「this」ポインターを使用してA :: fooを呼び出すことができ(タイプB *であっても)、害はありません。

同じ状況がクラスCの場合もあります。これはまだ単一の継承であり、通常のコンパイラはAのデータメンバーの直後にCのデータメンバーを配置するため、値を変更せずにC*をA*にすぐにキャストできます。したがって、コンパイラーは同じ「this」ポインターを使用してA :: fooを呼び出すことができ(タイプC *であっても)、害はありません。

ただし、クラスDの場合は状況がまったく異なります。通常、クラスDのレイアウトは、クラスAのデータメンバー、クラスBのデータメンバー、クラスCのデータメンバー、クラスDのデータメンバーの順になります。

通常のレイアウトを使用すると、D*をすぐにA*に変換できるため、A::fooにペナルティはありません。コンパイラは「this」を変更せずにA::foo用に生成したものと同じルーチンを呼び出すことができます。そして、すべてが大丈夫です。

ただし、C :: other_member_funcが非仮想であっても、コンパイラがC :: other_member_funcなどのメンバー関数を呼び出す必要がある場合は、状況が変わります。その理由は、コンパイラがC :: other_member_funcのコードを記述したときに、「this」ポインタによって参照されるデータレイアウトがAのデータメンバーの直後にCのデータメンバーが続くと想定したためです。ただし、これはDのインスタンスには当てはまりません。コンパイラは、クラスインスタンスのメモリレイアウトの違いを処理するために、(非仮想)D::other_member_funcを書き直して作成する必要がある場合があります。

多重継承を使用する場合、これは異なるが類似した状況であることに注意してください。ただし、仮想ベースのない多重継承では、コンパイラは、基本クラスがどこにあるかを説明するために、「this」ポインタに変位または修正を追加するだけですべてを処理できます。派生クラスのインスタンス内に「埋め込まれている」。ただし、仮想ベースでは、関数の書き換えが必要になる場合があります。それはすべて、呼び出されている(非仮想の)メンバー関数によってアクセスされるデータメンバーに依存します。

たとえば、クラスCが非仮想メンバー関数C :: some_member_funcを定義した場合、コンパイラーは次のように記述する必要があります。

  1. C :: some_member_funcは、コンパイル時に決定されたC(Dではなく)の実際のインスタンスから呼び出された場合(some_member_funcは仮想関数ではないため)
  2. C :: some_member_funcは、コンパイル時に決定された、クラスDの実際のインスタンスから同じメンバー関数が呼び出された場合。(技術的には、このルーチンはD::some_member_funcです。このメンバー関数の定義は暗黙的でC::some_member_funcのソースコードと同じですが、生成されるオブジェクトコードはわずかに異なる場合があります。)

C :: some_member_funcのコードが、クラスAとクラスCの両方で定義されたメンバー変数を使用している場合。

于 2018-02-16T09:05:28.823 に答える
0

仮想継承にはコストがかかる必要があります。

その証拠は、仮想的に継承されたクラスがパーツの合計よりも多くを占めることです。

典型的なケース:

struct A{double a;};

struct B1 : virtual A{double b1;};
struct B2 : virtual A{double b2;};

struct C : virtual B1, virtual B2{double c;}; // I think these virtuals are not strictly necessary
static_assert( sizeof(A) == sizeof(double) ); // as expected

static_assert( sizeof(B1) > sizeof(A) + sizeof(double) ); // the equality holds for non-virtual inheritance
static_assert( sizeof(B2) > sizeof(A) + sizeof(double) );  // the equality holds for non-virtual inheritance
static_assert( sizeof(C) > sizeof(A) + sizeof(double) + sizeof(double) + sizeof(double) );
static_assert( sizeof(C) > sizeof(A) + sizeof(double) + sizeof(double) + sizeof(double) + sizeof(double));

https://godbolt.org/z/zTcfoY

追加で何が保存されますか?よくわかりません。仮想テーブルのようなものですが、個々のメンバーにアクセスするためのものだと思います。

于 2020-11-18T04:48:55.463 に答える