あなたの質問は主に仮想ベースの通常の関数を呼び出すことに焦点を当てており、仮想ベースクラス(あなたの例ではクラス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を定義した場合、コンパイラーは次のように記述する必要があります。
- C :: some_member_funcは、コンパイル時に決定されたC(Dではなく)の実際のインスタンスから呼び出された場合(some_member_funcは仮想関数ではないため)
- C :: some_member_funcは、コンパイル時に決定された、クラスDの実際のインスタンスから同じメンバー関数が呼び出された場合。(技術的には、このルーチンはD::some_member_funcです。このメンバー関数の定義は暗黙的でC::some_member_funcのソースコードと同じですが、生成されるオブジェクトコードはわずかに異なる場合があります。)
C :: some_member_funcのコードが、クラスAとクラスCの両方で定義されたメンバー変数を使用している場合。