仮想関数がC++にあることは誰もが知っていますが、どのように深いレベルで実装されていますか?
Vtableを変更したり、実行時に直接アクセスしたりすることはできますか?
Vtableはすべてのクラスに存在しますか、または少なくとも1つの仮想関数を持つクラスのみに存在しますか?
抽象クラスは、少なくとも1つのエントリの関数ポインターにNULLを単に持っていますか?
単一の仮想関数があると、クラス全体が遅くなりますか?または、仮想関数への呼び出しのみですか?また、仮想関数が実際に上書きされるかどうかによって速度が影響を受けますか、または仮想関数である限りこれは効果がありません。
「C++の仮想関数」 から:
プログラムで仮想関数が宣言されている場合は常に、クラスのvテーブルが構築されます。 vテーブルは、1つ以上の仮想関数を含むクラスの仮想関数のアドレスで構成されます。仮想関数を含むクラスのオブジェクトには、メモリ内の仮想テーブルのベースアドレスを指す仮想ポインタが含まれています。仮想関数呼び出しがある場合は常に、vテーブルを使用して関数アドレスに解決します。 1つ以上の仮想関数を含むクラスのオブジェクトには、メモリ内のオブジェクトの先頭にvptrと呼ばれる仮想ポインターが含まれます。したがって、この場合のオブジェクトのサイズは、ポインターのサイズによって増加します。このvptrには、メモリ内の仮想テーブルのベースアドレスが含まれています。仮想テーブルはクラス固有であることに注意してください。つまり、クラスに含まれる仮想関数の数に関係なく、クラスに対して1つの仮想テーブルしかありません。この仮想テーブルには、クラスの1つ以上の仮想関数のベースアドレスが含まれています。仮想関数がオブジェクトで呼び出されると、そのオブジェクトのvptrはメモリ内のそのクラスの仮想テーブルのベースアドレスを提供します。このテーブルは、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。
普遍的に、答えは「ノー」だと思います。メモリをマングリングしてvtableを見つけることはできますが、それでも関数シグネチャがどのように呼び出されるかはわかりません。この機能(言語がサポートする機能)で実現したいことはすべて、vtableに直接アクセスしたり、実行時に変更したりすることなく可能になります。また、C++言語仕様はvtableが必要であることを指定していません-ただし、ほとんどのコンパイラは仮想関数を実装しています。
私はbelieve答えは「実装に依存する」ということです。なぜなら、仕様はそもそもvtableを必要としないからです。ただし、実際には、クラスに1つ以上の仮想関数がある場合にのみ、最新のコンパイラはすべてvtableを作成すると考えています。 vtableに関連するスペースのオーバーヘッドと、仮想関数と非仮想関数の呼び出しに関連する時間のオーバーヘッドがあります。
答えは、言語仕様で指定されていないため、実装に依存します。純粋仮想関数を呼び出すと、定義されていない場合(通常は定義されていない)、未定義の動作が発生します(ISO/IEC 14882:2003 10.4-2)。実際には、関数のvtableにスロットを割り当てますが、アドレスを割り当てません。これにより、vtableが不完全なままになり、派生クラスが関数を実装してvtableを完了する必要があります。一部の実装では、単にvtableエントリにNULLポインターを配置します。他の実装では、アサーションに似た動作を行うダミーメソッドへのポインターを配置します。
抽象クラスは純粋な仮想関数の実装を定義できますが、その関数は修飾ID構文(つまり、派生クラス)。これは、使いやすいデフォルトの実装を提供するために行われますが、派生クラスがオーバーライドを提供する必要があります。
これは私の知識の端に到達しているので、私が間違っているなら誰かがここで私を助けてください!
Ibelieveクラス内で仮想の関数のみが、仮想関数と非仮想関数の呼び出しに関連する時間パフォーマンスヒットを経験すると信じています。クラスのスペースのオーバーヘッドはどちらの方法でもあります。 vtableがある場合、classごとに1つだけであり、objectごとに1つはないことに注意してください。
オーバーライドされた仮想関数の実行時間が、ベース仮想関数の呼び出しと比較して減少するとは思わない。ただし、派生クラスと基本クラスの別のvtableの定義に関連するクラスには、追加のスペースオーバーヘッドがあります。
http://www.codersource.net/published/view/325/virtual_functions_in.aspx (ウェイバックマシン経由)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable
移植性はありませんが、汚いトリックを気にしないなら、確かに!
[〜#〜] warning [〜#〜]:このテクニックは、 969歳未満の子供、大人による使用は推奨されません 、またはアルファケンタウリの小さな毛皮のような生き物。副作用には、 鼻から飛び出す悪魔 、以降のすべてのコードレビューの必須承認者としての Yog-Sothoth の突然の出現、または-の遡及的な追加が含まれます。
IHuman::PlayPiano()
すべての既存のインスタンス]
私が見たほとんどのコンパイラでは、vtbl *はオブジェクトの最初の4バイトであり、vtblの内容は単純にメンバーポインターの配列です(通常は宣言された順序で、基本クラスが最初です)。もちろん、他の可能なレイアウトもありますが、それは私が一般的に観察していることです。
class A {
public:
virtual int f1() = 0;
};
class B : public A {
public:
virtual int f1() { return 1; }
virtual int f2() { return 2; }
};
class C : public A {
public:
virtual int f1() { return -1; }
virtual int f2() { return -2; }
};
A *x = new B;
A *y = new C;
A *z = new C;
シェナンガンを引くために...
実行時にクラスを変更する:
std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!
すべてのインスタンスのメソッドを置き換える(クラスのモンキーパッチ)
Vtbl自体はおそらく読み取り専用メモリ内にあるため、これは少し複雑です。
int f3(A*) { return 0; }
mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0
後者は、mprotectの操作により、ウイルスチェッカーとリンクが起動して通知を受け取る可能性があります。 NXビットを使用するプロセスでは、失敗する可能性があります。
または、仮想関数の呼び出しのみですか?また、仮想関数が実際に上書きされるかどうかによって速度が影響を受けますか、または仮想関数である限りこれは効果がありません。
仮想関数があると、そのようなクラスのオブジェクトを処理するときに、データのもう1つの項目を初期化、コピーする必要がある限り、クラス全体の速度が低下します。半ダース程度のクラスのクラスの場合、違いは無視できるはずです。単一のchar
メンバーを含むクラス、またはメンバーがまったくないクラスの場合、違いは顕著です。
それとは別に、仮想関数へのすべての呼び出しが仮想関数呼び出しではないことに注意することが重要です。既知の型のオブジェクトがある場合、コンパイラーは通常の関数呼び出しのコードを出力できます。また、必要に応じてその関数をインライン化することもできます。基本クラスのオブジェクトまたは派生クラスのオブジェクトを指す可能性のあるポインターまたは参照を介してポリモーフィックコールを行う場合にのみ、vtableインダイレクションが必要であり、パフォーマンスの観点から支払います。
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
機能が上書きされるかどうかに関係なく、ハードウェアがとらなければならないステップは基本的に同じです。 vtableのアドレスはオブジェクトから読み取られ、関数ポインターは適切なスロットから取得され、関数はポインターによって呼び出されます。実際のパフォーマンスの観点では、分岐予測が何らかの影響を与える可能性があります。したがって、たとえば、ほとんどのオブジェクトが特定の仮想関数の同じ実装を参照している場合、ポインターが取得される前であっても、分岐予測子が呼び出す関数を正しく予測する可能性があります。ただし、どの関数が一般的なものであるかは問題ではありません。ほとんどのオブジェクトが上書きされないベースケースに委任されるか、ほとんどのオブジェクトが同じサブクラスに属し、したがって同じ上書きされたケースに委任されます。
モックの実装を使用してこれを実証するjherikoのアイデアが好きです。しかし、Cを使用して上記のコードに似たものを実装すると、低レベルがより簡単に見えるようになります。
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
ご覧のとおり、vtableはメモリ内の単なる静的ブロックであり、ほとんどが関数ポインターを含んでいます。多相クラスのすべてのオブジェクトは、その動的型に対応するvtableを指します。これにより、RTTIと仮想関数間の接続がより明確になります。クラスが指しているvtableを調べるだけで、クラスのタイプを確認できます。上記は多くの点で単純化されています。多重継承ですが、一般的な概念は健全です。
arg
のタイプがFoo*
であり、arg->vtable
を使用するが、実際はBar
タイプのオブジェクトである場合、vtable
の正しいアドレスを取得します。 。これは、正しく型付けされた式でvtable
またはbase.vtable
と呼ばれるかどうかに関係なく、vtable
が常にオブジェクトのアドレスの最初の要素であるためです。
以下は、最新のC++での仮想テーブルのrunnable手動実装です。明確に定義されたセマンティクスを持ち、ハッキングもvoid*
もありません。
注:.*
および->*
は、*
および->
とは異なる演算子です。メンバー関数ポインターの動作は異なります。
#include <iostream>
#include <vector>
#include <memory>
struct vtable; // forward declare, we need just name
class animal
{
public:
const std::string& get_name() const { return name; }
// these will be abstract
bool has_tail() const;
bool has_wings() const;
void sound() const;
protected: // we do not want animals to be created directly
animal(const vtable* vtable_ptr, std::string name)
: vtable_ptr(vtable_ptr), name(std::move(name)) { }
private:
friend vtable; // just in case for non-public methods
const vtable* const vtable_ptr;
std::string name;
};
class cat : public animal
{
public:
cat(std::string name);
// functions to bind dynamically
bool has_tail() const { return true; }
bool has_wings() const { return false; }
void sound() const
{
std::cout << get_name() << " does meow\n";
}
};
class dog : public animal
{
public:
dog(std::string name);
// functions to bind dynamically
bool has_tail() const { return true; }
bool has_wings() const { return false; }
void sound() const
{
std::cout << get_name() << " does whoof\n";
}
};
class parrot : public animal
{
public:
parrot(std::string name);
// functions to bind dynamically
bool has_tail() const { return false; }
bool has_wings() const { return true; }
void sound() const
{
std::cout << get_name() << " does crrra\n";
}
};
// now the magic - pointers to member functions!
struct vtable
{
bool (animal::* const has_tail)() const;
bool (animal::* const has_wings)() const;
void (animal::* const sound)() const;
// constructor
vtable (
bool (animal::* const has_tail)() const,
bool (animal::* const has_wings)() const,
void (animal::* const sound)() const
) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};
// global vtable objects
const vtable vtable_cat(
static_cast<bool (animal::*)() const>(&cat::has_tail),
static_cast<bool (animal::*)() const>(&cat::has_wings),
static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
static_cast<bool (animal::*)() const>(&dog::has_tail),
static_cast<bool (animal::*)() const>(&dog::has_wings),
static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
static_cast<bool (animal::*)() const>(&parrot::has_tail),
static_cast<bool (animal::*)() const>(&parrot::has_wings),
static_cast<void (animal::*)() const>(&parrot::sound));
// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }
// implement dynamic dispatch
bool animal::has_tail() const
{
return (this->*(vtable_ptr->has_tail))();
}
bool animal::has_wings() const
{
return (this->*(vtable_ptr->has_wings))();
}
void animal::sound() const
{
(this->*(vtable_ptr->sound))();
}
int main()
{
std::vector<std::unique_ptr<animal>> animals;
animals.Push_back(std::make_unique<cat>("grumpy"));
animals.Push_back(std::make_unique<cat>("nyan"));
animals.Push_back(std::make_unique<dog>("doge"));
animals.Push_back(std::make_unique<parrot>("party"));
for (const auto& a : animals)
a->sound();
// note: destructors are not dispatched virtually
}
私はそれを簡単にしようとします:)
これは、特定の仮想関数の実装である関数へのポインターを持つ配列です。この配列のインデックスは、クラスに定義された仮想関数の特定のインデックスを表します。これには、純粋仮想関数が含まれます。
ポリモーフィッククラスが別のポリモーフィッククラスから派生する場合、次の状況が発生する可能性があります。
標準的な方法ではありません-それらにアクセスするためのAPIはありません。コンパイラには、アクセスするためのいくつかの拡張機能またはプライベートAPIがありますが、それは単なる拡張機能である場合があります。
少なくとも1つの仮想関数(デストラクタであっても)を持つもの、またはvtableを持つクラスを少なくとも1つ派生するもの(「ポリモーフィック」)。
それは可能な実装ですが、実践されていません。代わりに、「呼び出された純粋な仮想関数」のようなものを出力し、abort()
を実行する関数が通常あります。コンストラクターまたはデストラクターで抽象メソッドを呼び出そうとすると、その呼び出しが発生する場合があります。
スローダウンは、コールが直接コールとして解決されるか、仮想コールとして解決されるかによってのみ異なります。そして他には何の問題。 :)
ポインターまたはオブジェクトへの参照を介して仮想関数を呼び出す場合、それは常に仮想呼び出しとして実装されます-コンパイラーはランタイムでこのポインターに割り当てられるオブジェクトの種類と、それがこのメソッドがオーバーライドされるかどうかのクラス。次の2つの場合にのみ、コンパイラは仮想関数への呼び出しを直接呼び出しとして解決できます。
final
として宣言されている場合(C++ 11のみ)。この場合、コンパイラは、このメソッドがそれ以上オーバーライドできないことと、このクラスのメソッドにしかなれないことを知っています。ただし、仮想呼び出しには2つのポインターを間接参照するオーバーヘッドしかありません。 RTTIの使用(ポリモーフィッククラスでのみ使用可能)は、仮想メソッドの呼び出しよりも遅くなります。同じ方法を2つの方法で実装する場合があります。たとえば、virtual bool HasHoof() { return false; }
を定義し、bool Horse::HasHoof() { return true; }
としてのみオーバーライドすると、if (anim->HasHoof())
を呼び出すよりも高速なif(dynamic_cast<Horse*>(anim))
を呼び出すことができます。 。それの訳は dynamic_cast
は、実際のポインター型と目的のクラス型からパスを構築できるかどうかを確認するために、場合によっては再帰的にクラス階層をたどる必要があります。仮想呼び出しは常に同じですが、2つのポインターを逆参照します。
この回答は Community Wiki answer に組み込まれました
その答えは、指定されていないということです-純粋仮想関数を呼び出すと、定義されていない場合(通常は定義されていない)、未定義の動作になります(ISO/IEC 14882:2003 10.4-2)。一部の実装では、単にvtableエントリにNULLポインターを配置します。他の実装では、アサーションと同様のことを行うダミーメソッドへのポインターを配置します。
抽象クラスは純粋な仮想関数の実装を定義できますが、その関数は修飾ID構文でのみ呼び出すことができます(つまり、メソッド名でクラスを完全に指定し、派生クラス)。これは、派生クラスがオーバーライドを提供することを必要とする一方で、使いやすいデフォルトの実装を提供するために行われます。
クラスのメンバーとして関数ポインターを使用し、実装として静的関数を使用するか、実装のメンバー関数およびメンバー関数へのポインターを使用して、C++で仮想関数の機能を再作成できます。 2つのメソッド間には表記上の利点しかありません...実際、仮想関数呼び出しは単なる表記上の利便性です。実際、継承は単なる表記上の便利さです...継承に言語機能を使用せずにすべて実装できます。 :)
以下はテストされていないが、おそらくバグのあるコードですが、うまくいけばアイデアを示しています。
例えば.
class Foo
{
protected:
void(*)(Foo*) MyFunc;
public:
Foo() { MyFunc = 0; }
void ReplciatedVirtualFunctionCall()
{
MyFunc(*this);
}
...
};
class Bar : public Foo
{
private:
static void impl1(Foo* f)
{
...
}
public:
Bar() { MyFunc = impl1; }
...
};
class Baz : public Foo
{
private:
static void impl2(Foo* f)
{
...
}
public:
Baz() { MyFunc = impl2; }
...
};
通常、VTableで、関数へのポインターの配列。
これらのすべての答えでここで言及されていないことは、複数の継承の場合、基本クラスにすべて仮想メソッドがあるということです。継承クラスには、vmtへの複数のポインターがあります。その結果、そのようなオブジェクトの各インスタンスのサイズが大きくなります。仮想メソッドを持つクラスにはvmt用に余分な4バイトがあることは誰もが知っていますが、多重継承の場合は、仮想メソッドx 4の各基本クラスがポインターのサイズになります。
各オブジェクトには、メンバー関数の配列を指すvtableポインターがあります。
概念の非常にかわいい証明は少し前に作成しました(継承の順序が重要かどうかを確認するため)。あなたのC++の実装が実際にそれを拒否するかどうかを教えてください(私のバージョンのgccは匿名の構造体を割り当てるための警告を与えるだけですが、それはバグです)、私は興味があります。
CCPolite.h:
#ifndef CCPOLITE_H
#define CCPOLITE_H
/* the vtable or interface */
typedef struct {
void (*Greet)(void *);
void (*Thank)(void *);
} ICCPolite;
/**
* the actual "object" literal as C++ sees it; public variables be here too
* all CPolite objects use(are instances of) this struct's structure.
*/
typedef struct {
ICCPolite *vtbl;
} CPolite;
#endif /* CCPOLITE_H */
CCPolite_constructor.h:
/**
* unconventionally include me after defining OBJECT_NAME to automate
* static(allocation-less) construction.
*
* note: I assume CPOLITE_H is included; since if I use anonymous structs
* for each object, they become incompatible and cause compile time errors
* when trying to do stuff like assign, or pass functions.
* this is similar to how you can't pass void * to windows functions that
* take handles; these handles use anonymous structs to make
* HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
* require a cast.
*/
#ifndef OBJECT_NAME
#error CCPolite> constructor requires object name.
#endif
CPolite OBJECT_NAME = {
&CCPolite_Vtbl
};
/* ensure no global scope pollution */
#undef OBJECT_NAME
main.c:
#include <stdio.h>
#include "CCPolite.h"
// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
virtual void Greet() = 0;
};
// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
virtual void Thank() = 0;
};
// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};
// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
void Greet()
{
puts("hello!");
}
void Thank()
{
puts("thank you!");
}
};
// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
void Greet()
{
puts("hi!");
}
void Thank()
{
puts("ty!");
}
};
// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
puts("HI I AM C!!!!");
}
// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
puts("THANK YOU, I AM C!!");
}
// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
CCPolite_Thank,
CCPolite_Greet
};
CPolite CCPoliteObj = {
&CCPolite_Vtbl
};
int main(int argc, char **argv)
{
puts("\npart 1");
CPolite1 o1;
o1.Greet();
o1.Thank();
puts("\npart 2");
CPolite2 o2;
o2.Greet();
o2.Thank();
puts("\npart 3");
CPolite1 *not1 = (CPolite1 *)&o2;
CPolite2 *not2 = (CPolite2 *)&o1;
not1->Greet();
not1->Thank();
not2->Greet();
not2->Thank();
puts("\npart 4");
CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
fake->Thank();
fake->Greet();
puts("\npart 5");
CPolite2 *fake2 = (CPolite2 *)fake;
fake2->Thank();
fake2->Greet();
puts("\npart 6");
#define OBJECT_NAME fake3
#include "CCPolite_constructor.h"
fake = (CPolite1 *)&fake3;
fake->Thank();
fake->Greet();
puts("\npart 7");
#define OBJECT_NAME fake4
#include "CCPolite_constructor.h"
fake2 = (CPolite2 *)&fake4;
fake2->Thank();
fake2->Greet();
return 0;
}
出力:
part 1
hello!
thank you!
part 2
hi!
ty!
part 3
ty!
hi!
thank you!
hello!
part 4
HI I AM C!!!!
THANK YOU, I AM C!!
part 5
THANK YOU, I AM C!!
HI I AM C!!!!
part 6
HI I AM C!!!!
THANK YOU, I AM C!!
part 7
THANK YOU, I AM C!!
HI I AM C!!!!
偽のオブジェクトを割り当てることはないため、破棄する必要はありません。デストラクタは、動的に割り当てられたオブジェクトのスコープの最後に自動的に配置され、オブジェクトリテラル自体とvtableポインタのメモリを再利用します。
Burlyの答えは、質問を除いてここでは正しいです。
抽象クラスは、少なくとも1つのエントリの関数ポインタにNULLを単に持っていますか?
答えは、抽象クラスの仮想テーブルはまったく作成されないということです。これらのクラスのオブジェクトは作成できないため、必要はありません!
言い換えれば:
class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class
D* pD = new D();
B* pB = pD;
PBを介してアクセスされるvtblポインターは、クラスDのvtblになります。これがまさにポリモーフィズムの実装方法です。つまり、pBを介してDメソッドにアクセスする方法です。クラスBのvtblは不要です。
説明のBクラスに仮想メソッドfoo()があり、Dによってオーバーライドされていない場合、仮想メソッドbar()にオーバーライドされている場合、DのvtblはBのfoo()および独自のbar()へのポインター。 B用に作成されたvtblはまだありません。