1 つ以上の仮想関数を含むすべてのクラスには、関連付けられた Vtable があります。vptr と呼ばれる void ポインターは、その vtable を指します。そのクラスのすべてのオブジェクトには、同じ Vtable を指す vptr が含まれています。では、なぜ vptr static ではないのでしょうか? vptr をオブジェクトに関連付ける代わりに、クラスに関連付けてみませんか?
7 に答える
オブジェクトのランタイム クラスは、オブジェクト自体のプロパティです。実際、vptr
はランタイム クラスを表すため、 にすることはできませんstatic
。ただし、それが指すものは、同じランタイム クラスのすべてのインスタンスで共有できます。
あなたの図は間違っています。vtable は 1 つではなく、ポリモーフィック タイプごとに 1 つの vtable があります。の vptr はA
の vtable を指しA
、 の vptr はA1
の vtable を指しますA1
。
与えられた:
class A {
public:
virtual void foo();
virtual void bar();
};
class A1 : public A {
virtual void foo();
};
class A2 : public A {
virtual void foo();
};
class A3 : public A {
virtual void bar();
virtual void baz();
};
を含む
vtable をA
含む
vtable を含むvtable を含む
vtable{ &A::foo, &A::bar }
A1
{ &A1::foo, &A::bar }
A2
{ &A2::foo, &A::bar }
A3
{ &A::foo, &A3::bar, &A3::baz }
そのため、コンパイラを呼び出すとa.foo()
、オブジェクトの vptr に従って vtable が検出され、vtable の最初の関数が呼び出されます。
コンパイラがあなたのアイデアを使用し、次のように書くとします。
A1 a1;
A2 a2;
A& a = (std::rand() % 2) ? a1 : a2;
a.foo();
コンパイラは基本クラスを調べて、参照がバインドされているオブジェクトのメンバーではなく、型のプロパティでA
あるクラスの vptr を見つけA
ます。その vptr は、 、または、またはその他の vtable を指していますか? それが vtable を指している場合、 を参照するときに 50% の確率で間違っていることになり、その逆も同様です。static
A
a
A
A1
A2
A1
a
a2
ここで、次のように書くとします。
A1 a1;
A2 a2;
A& a = a1;
A& aa = a2;
a.foo();
aa.foo();
a
とaa
はどちらも への参照ですがA
、2 つの異なる vptrs が必要です。1 つは vtable forA1
を指し、もう 1 つは の vtable を指しA2
ます。vptr が の静的メンバーである場合、A
どのようにして一度に 2 つの値を持つことができますか? 唯一の論理的で一貫した選択は、 の静的 vptr がA
の vtable を指すことですA
。
しかし、それは、呼び出すa.foo()
べきA::foo()
ときに呼び出しが呼び出され、呼び出す必要があるときにA1::foo()
呼び出しaa.foo()
も呼び出すことを意味します。A::foo()
A2::foo()
あなたのアイデアが必要なセマンティクスを実装していないことは明らかです。これは、あなたのアイデアを使用するコンパイラが C++ コンパイラではないことを証明しています。派生型が何であるかを知らずに、コンパイラが vtable を取得する方法はありませんA1
(a
これは一般に不可能です。ベースへの参照は、別のライブラリで定義された関数から返された可能性があり、参照することができます)。まだ書かれていない派生型!) または vptr をオブジェクトに直接格納することによって。
vptr は と で異なる必要がa1
ありa2
、ポインターまたは base への参照を介してそれらにアクセスする場合、動的な型を知らなくてもアクセスできる必要がありますa
。基底クラスの vtable ではありません。これを行う最も明白な方法は、vptr をオブジェクトに直接格納することです。別の、より複雑な解決策は、オブジェクト アドレスの vptrs へのマップ (たとえば のようなもの) を保持し、検索しstd::map<void*, vtable*>
て vtable を見つけることです。a
&a
、しかし、これはタイプごとではなくオブジェクトごとに 1 つの vptr を保存し、ポリモーフィック オブジェクトが作成および破棄されるたびにマップを更新するためにより多くの作業 (および動的割り当て) が必要になり、マップ構造がスペースを取ります。vptr をオブジェクト自体に埋め込むだけの方が簡単です。
仮想テーブル (ちなみに、これは C++ 標準では言及されていない実装メカニズムです) は、実行時にオブジェクトの動的な型を識別するために使用されます。したがって、オブジェクト自体はそれへのポインターを保持する必要があります。それが静的である場合、それによって識別できるのは静的タイプだけであり、役に立たないでしょう。
何らかの方法でtypeid()
内部的に使用して動的型を識別し、それを使用して静的ポインターを呼び出すことを考えているtypeid()
場合は、仮想関数を持つ型に属するオブジェクトの動的型のみを返すことに注意してください。それ以外の場合は、静的型を返すだけです (現在の C++ 標準の§ 5.2.8)。はい、これは逆に動作することを意味します。typeid()
通常、仮想ポインターを使用して動的型を識別します。
要点はvptr
、オブジェクトが実行時にどのクラスを持っているか正確にわからないためです。それが分かっていれば、仮想関数呼び出しは不要です。実際、これは仮想関数を使用していないときに起こることです。しかし、仮想関数を使用すると、
class Sub : Parent {};
と type の値、これが本当に type のオブジェクトなのかParent*
type のオブジェクトなのか、実行時にわかりません。vptr を使用すると、それを理解できます。Parent
Sub
仮想メソッドテーブルはクラスごとです。オブジェクトには、実行時型vptrへのポインターが含まれています。
これは、私が使用したすべてのコンパイルの標準バストの要件ではないと思います。
これはあなたの例にも当てはまります。