仮想クラスのすべてのオブジェクトは vtable へのポインタを持っていますか?
それとも、仮想関数を持つ基本クラスのオブジェクトだけがそれを持っていますか?
vtable はどこに保存されましたか? プロセスのコードセクションまたはデータセクション?
仮想クラスのすべてのオブジェクトは vtable へのポインタを持っていますか?
それとも、仮想関数を持つ基本クラスのオブジェクトだけがそれを持っていますか?
vtable はどこに保存されましたか? プロセスのコードセクションまたはデータセクション?
仮想メソッドを持つすべてのクラスには、クラスのすべてのオブジェクトによって共有される単一の vtable があります。
各オブジェクト インスタンスには、通常は vptr と呼ばれるその vtable へのポインター (vtable の検索方法) があります。コンパイラは、コンストラクターで vptr を初期化するコードを暗黙的に生成します。
これは C++ 言語によって義務付けられていないことに注意してください。必要に応じて、実装で仮想ディスパッチを別の方法で処理できます。ただし、これは、私がよく知っているすべてのコンパイラで使用される実装です。Stan Lippman の著書「Inside the C++ Object Model」では、これがどのようにうまく機能するかが説明されています。
他の誰かが言ったように、C++ 標準は仮想メソッド テーブルを義務付けていませんが、使用を許可しています。gcc とこのコード、および可能な最も単純なシナリオの 1 つを使用してテストを行いました。
class Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived1 : public Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived2 : public Base {
public:
virtual void smile() { }
int dont_do_ebo;
};
void use(Base* );
int main() {
Base * b = new Derived1;
use(b);
Base * b1 = new Derived2;
use(b1);
}
コンパイラが基本クラスにゼロのサイズを与えるのを防ぐために、データ メンバーを追加しました (これは、空の基本クラスの最適化として知られています)。これは GCC が選択したレイアウトです: (-fdump-class-hierarchy を使用して印刷)
Vtable for Base
Base::_ZTV4Base: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI4Base)
8 Base::bark
Class Base
size=8 align=4
base size=8 base align=4
Base (0xb7b578e8) 0
vptr=((& Base::_ZTV4Base) + 8u)
Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived1)
8 Derived1::bark
Class Derived1
size=12 align=4
base size=12 base align=4
Derived1 (0xb7ad6400) 0
vptr=((& Derived1::_ZTV8Derived1) + 8u)
Base (0xb7b57ac8) 0
primary-for Derived1 (0xb7ad6400)
Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived2)
8 Base::bark
12 Derived2::smile
Class Derived2
size=12 align=4
base size=12 base align=4
Derived2 (0xb7ad64c0) 0
vptr=((& Derived2::_ZTV8Derived2) + 8u)
Base (0xb7b57c30) 0
primary-for Derived2 (0xb7ad64c0)
ご覧のとおり、各クラスには vtable があります。最初の 2 つのエントリは特別です。2 番目のものは、クラスの RTTI データを指します。最初のもの - 知っていましたが、忘れていました。より複雑なケースでは、ある程度の用途があります。レイアウトが示すように、Derived1 クラスのオブジェクトがある場合、vptr (v-table-pointer) はもちろん Derived1 クラスの v-table を指します。 Derived1 のバージョン。Derived2 の vptr は、2 つのエントリを持つ Derived2 の vtable を指します。もう一つは、それによって追加された新しいメソッドです、笑。これは Base::bark のエントリを繰り返します。これはもちろん、関数の最も派生したバージョンであるため、関数の Base バージョンを指します。
-fdump-tree-optimized を使用して、いくつかの最適化 (コンストラクターのインライン化など) が行われた後に GCC によって生成されたツリーもダンプしました。GIMPL
出力は、フロントエンドに依存しないGCC のミドルエンド言語を使用しており、C に似たブロック構造にインデントされています。
;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
return;
}
;; Function int main() (main)
int main() ()
{
void * D.1757;
struct Derived2 * D.1734;
void * D.1756;
struct Derived1 * D.1693;
<bb 2>:
D.1756 = operator new (12);
D.1693 = (struct Derived1 *) D.1756;
D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
use (&D.1693->D.1671);
D.1757 = operator new (12);
D.1734 = (struct Derived2 *) D.1757;
D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
use (&D.1734->D.1682);
return 0;
}
よくわかるように、1 つのポインター (vptr) を設定するだけです。このポインターは、オブジェクトの作成時に以前に見た適切な vtable を指します。Derived1 と call to use ($4 は最初の引数のレジスタ、$2 は戻り値のレジスタ、$0 は常に 0 のレジスタ) を作成するためのアセンブラ コードもダンプしましたc++filt
。
# 1st arg: 12byte
add $4, $0, 12
# allocate 12byte
jal operator new(unsigned long)
# get ptr to first function in the vtable of Derived1
add $3, $0, vtable for Derived1+8
# store that pointer at offset 0x0 of the object (vptr)
stw $3, $2, 0
# 1st arg is the address of the object
add $4, $0, $2
jal use(Base*)
を呼び出したい場合はどうなりますbark
か?:
void doit(Base* b) {
b->bark();
}
GIMPL コード:
;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
return;
}
OBJ_TYPE_REF
きれいに印刷されたGIMPLコンストラクトです(gcc/tree.def
gcc SVNソースコードに記載されています)
OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)
意味: *b->_vptr.Base
objectb
で式を使用し、フロントエンド (c++) 固有の値0
(vtable へのインデックス) を格納します。b
最後に、「this」引数として渡します。vtable の 2 番目のインデックスに表示される関数を呼び出す場合 (注: どのタイプの vtable かはわかりません!)、GIMPL は次のようになります。
OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];
もちろん、ここで再びアセンブリ コードを示します (スタック フレームのものは省略されています)。
# load vptr into register $2
# (remember $4 is the address of the object,
# doit's first arg)
ldw $2, $4, 0
# load whatever is stored there into register $2
ldw $2, $2, 0
# jump to that address. note that "this" is passed by $4
jalr $2
vptr が正確に最初の関数を指していることを思い出してください。(そのエントリの前に、RTTI スロットが格納されていました)。したがって、そのスロットに表示されるものはすべて呼び出されます。また、呼び出しは関数の最後のステートメントとして発生するため、呼び出しを末尾呼び出しとしてマークしていdoit
ます。
Vtableはクラスごとのインスタンスです。つまり、仮想メソッドを持つクラスのオブジェクトが10個ある場合、10個のオブジェクトすべてで共有されるvtableは1つだけです。
この場合の10個のオブジェクトはすべて、同じvtableを指しています。
自宅でこれを試してください:
#include <iostream>
struct non_virtual {};
struct has_virtual { virtual void nop() {} };
struct has_virtual_d : public has_virtual { virtual void nop() {} };
int main(int argc, char* argv[])
{
std::cout << sizeof non_virtual << "\n"
<< sizeof has_virtual << "\n"
<< sizeof has_virtual_d << "\n";
}
どのオブジェクト (以降のインスタンス) が vtable を持ち、どこにあるのかという質問に答えるには、いつ vtable ポインターが必要になるかを考えると役に立ちます。
どの継承階層でも、その階層内の特定のクラスによって定義された仮想関数のセットごとに vtable が必要です。つまり、次のようになります。
class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };
その結果、5 つの vtables が必要になります。A、B、C、D、および E にはすべて独自の vtable が必要です。
次に、特定のクラスへのポインターまたは参照を指定して、どの vtable を使用するかを知る必要があります。たとえば、A へのポインターが与えられた場合、A::f() をディスパッチする場所を示す vtable を取得できるように、A のレイアウトについて十分に知る必要があります。B へのポインターが与えられた場合、B::f() および B::g() をディスパッチするには、B のレイアウトについて十分に理解する必要があります。などなど。
考えられる実装の 1 つは、任意のクラスの最初のメンバーとして vtable ポインターを置くことができます。つまり、A のインスタンスのレイアウトは次のようになります。
A's vtable;
int a;
B のインスタンスは次のようになります。
A's vtable;
int a;
B's vtable;
int b;
そして、このレイアウトから正しい仮想ディスパッチ コードを生成できます。
同じレイアウトを持つ vtable の vtable ポインターを組み合わせることによって、または一方が他方のサブセットである場合、レイアウトを最適化することもできます。上記の例では、B を次のようにレイアウトすることもできます。
B's vtable;
int a;
int b;
B の vtable は A のスーパーセットであるためです。B の vtable には A::f と B::g のエントリがあり、A の vtable には A::f のエントリがあります。
完全を期すために、これまで見てきたすべての vtable を次のようにレイアウトします。
A's vtable: A::f
B's vtable: A::f, B::g
C's vtable: A::f, B::g, C::h
D's vtable: A::f
E's vtable: A::f, B::g
実際のエントリは次のようになります。
A's vtable: A::f
B's vtable: B::f, B::g
C's vtable: C::f, C::g, C::h
D's vtable: D::f
E's vtable: E::f, B::g
多重継承の場合、同じ分析を行います。
class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };
結果のレイアウトは次のようになります。
A:
A's vtable;
int a;
B:
B's vtable;
int b;
C:
C's A vtable;
int a;
C's B vtable;
int b;
int c;
C への参照は A または B の参照に変換でき、仮想関数を C にディスパッチする必要があるため、A と互換性のある vtable へのポインターと、B と互換性のある vtable へのポインターが必要です。
このことから、特定のクラスが持つ vtable ポインターの数は、少なくとも (直接またはスーパークラスによって) 派生元のルート クラスの数であることがわかります。ルート クラスは、vtable を持つクラスから継承しない vtable を持つクラスです。
仮想継承は、別の間接的な要素をミックスに投入しますが、同じメトリックを使用して vtable ポインターの数を決定できます。
通常、すべての仮想クラスにはvtableがありますが、C ++標準では必須ではなく、ストレージメソッドはコンパイラに依存します。
ポリモーフィックタイプのすべてのオブジェクトには、Vtableへのポインターがあります。
VTableが格納される場所は、コンパイラによって異なります。
必ずしも
仮想関数を持つほぼすべてのオブジェクトには、1 つの v-table ポインターがあります。オブジェクトの派生元となる仮想関数を持つクラスごとに、v テーブル ポインターは必要ありません。
ただし、コードを十分に分析する新しいコンパイラは、場合によっては v テーブルを削除できる場合があります。
たとえば、単純なケースでは、抽象基本クラスの具体的な実装が 1 つしかない場合、コンパイラは、仮想関数が呼び出されるたびに常に正確に解決されるため、仮想呼び出しを通常の関数呼び出しに変更できることを認識しています。同じ機能。
また、異なる具象関数が 2 つしかない場合、コンパイラは呼び出しサイトを効果的に変更して、'if' を使用して呼び出す正しい具象関数を選択することができます。
したがって、このような場合、v-table は必要なく、オブジェクトに v-table がない可能性があります。