仮想テーブルはコンパイル時に作成されるのに、なぜこれを C++ の実行時ポリモーフィズムと呼ぶのでしょうか?
8 に答える
検索は実行時に行われるためです。
典型的な実装では、各クラスにはコンパイル時に認識される仮想テーブルがあります。
実行時に、 型のポインタは、型BaseClass *
が であるオブジェクトBaseClass
、または型が であるオブジェクトの基底クラスのサブオブジェクトを指している可能性があります。DerivedClass
ここで、BaseClass
は のベースですDerivedClass
。参考文献も同様です。
前者の場合、仮想呼び出しは の vtable で検索されBaseClass
ます。後者の場合、仮想呼び出しは の vtable で検索されDerivedClass
ます。呼び出しサイトは、呼び出しが実行時に実際に実行されるまでどの関数が呼び出されるかを「認識」しないため、これは動的ポリモーフィズムまたはランタイム ポリモーフィズムと呼ばれます。
再び典型的な実装では、使用する vtable を見つける方法は、1 つ以上の仮想関数を持つ型のオブジェクトに、その完全な型の vtable を指す「隠し」追加フィールドが含まれていることです。それは単純な継承のためです。多重継承と仮想継承は複雑さを増しますが、原則は同じです。オブジェクトは、使用する vtable へのポインタを提供します。
これを、コンパイラが vtable を使用したり、完全なオブジェクトの型を知る必要がない非仮想呼び出しと比較してください。ポインタまたは参照の型に応じて関数を選択します。
仮想テーブルは関係ありません。C ++のランタイムポリモーフィズムとは、次のことを意味します。
struct B {
virtual void f() { std::cout << "In B\n"; }
};
struct D1 : B {
virtual void f() { std::cout << "In D1\n"; }
};
struct D2 : b {
virtual void f() { std::cout << "In D2\n"; }
};
B *bp = new B;
bp->f(); // calls B::f
B *bp1 = new D1;
bp1->f(); // calls D1::f
B *bp2 = new D2;
bp2->f(); // calls D2::f
3つのポインターすべてにタイプB*
がありますが、への呼び出しの動作はf()
、ポインターが指すオブジェクトの実行時タイプによって異なります。
あなたが言うように、各クラスの仮想関数テーブル(および他の多態的な情報)はコンパイル時に生成されます。
各オブジェクトには、動的タイプの正しいテーブルへのポインタが含まれています。このポインターは、オブジェクトが作成されるときに実行時に初期化され、実行時に呼び出す正しい仮想関数を選択するために使用されます。それが実行時ポリモーフィズムと呼ばれる理由です。
仮想テーブルは、 C++ およびその他のいくつかの OO 言語におけるクラス型の実行時表現の要素です。仮想メソッド呼び出しの動的ディスパッチに使用されます。つまり、C++ の動的ポリモーフィズム機能の実装の詳細です。
このテーブルが構築される時間は、ポリモーフィズムが静的か動的かを定義するディスパッチ スキームとは無関係です。
仮想テーブルはコンパイル中に作成されますが、実行時に使用されます。
そのためには、ランタイム ポリモーフィズムとは何か、またどのように機能するのかを簡単に理解する必要があります。
次のようなクラス階層がある場合:
class Animal
{
...
virtual void sayHello() {
cout << "hello" << endl;
}
};
class Dog : public Animal
{
...
/*virtual*/ void sayHello() {
cout << "woof!" << endl;
}
};
インスタンスでメソッドを呼び出す場合、ではなく のDog
(仮想) 上書きされたメソッドを呼び出せるようにする必要があります。これが必要な理由はご存知だと思いますので、これ以上説明したくありません。Dog
Animal
Animal*
しかし、実行時に anが実際に aであることをどのように知ることができますDog*
か? 問題は、それが何であるかを知る必要はなく、どの関数を呼び出すか、またはより適切に言えば、どの関数ポインタを呼び出すかだけを知る必要があるということです。仮想関数のすべての関数ポインタは、各クラスの仮想テーブルに格納されます。これは、実際のクラスに応じて、どの仮想関数に対してどのコードを呼び出すかについての「ガイドライン」と考えることができます。
この仮想テーブルはコンパイル時に作成され (コンパイラは「ガイドライン」を実行可能ファイルに書き込みます)、各インスタンスは使用可能な仮想テーブルの 1 つを指します。つまりDog *dog = new Dog
、Dog の仮想テーブルを指しているということです。のような呼び出しは、まだ指定されていないクラスdog->sayHello()
の仮想関数への仮想呼び出しにコンパイルされます...sayHello
次に、実行時に like を呼び出すdog->sayHello()
と、最初にオブジェクトに格納されている具体的な仮想テーブルが検索され (コードはそれが Dog であることを認識せず、Animal であることのみが認識されます)、 method の関数ポインターが検出されますDog::sayHello()
。
あなたの質問に答えるために、このメカニズムは実行時ポリモーフィズムと呼ばれます。これは、実行時に決定が行われている間に、 this ポインターをオーバーロードするメソッドを呼び出すことができるためです(つまり、呼び出したオブジェクトの型をオーバーロードすることを意味します) 。のように、コンパイラがオブジェクトの具体的な型を知ることができる対応するコンパイル時ポリモーフィズムを呼び出すことができます。Dog dog; dog.sayHello()
v-table がコンパイル時に作成されるのは事実ですが、次のコードがコンパイルされるとき、コンパイラはどの関数が呼び出されるかを知りません。
struct A {
virtual void f() { cout << "A::f" << endl;}
};
struct B : public A {
void f() { cout << "B::f" << endl;}
};
int main() {
A* b = new B();
b->f(); // prints "B::f", chosen at runtime
}
したがって、オブジェクトはコンパイル時と実行時の間で変化しませんが、メソッド B::f が選択されるのは実行時のみです。これは、コンパイラがオブジェクトの動的な型 (どのメソッドを呼び出すかを決定する) を認識していないためです。