仮想関数とvtableはどのように実装されますか?


109

私たちは皆、C ++の仮想関数を知っていますが、それらは深いレベルでどのように実装されますか?

実行時にvtableを変更したり、直接アクセスしたりできますか?

vtableはすべてのクラスに存在しますか、それとも少なくとも1つの仮想関数を持つクラスのみに存在しますか?

抽象クラスは、少なくとも1つのエントリの関数ポインタにNULLを持っているだけですか?

単一の仮想関数を使用すると、クラス全体が遅くなりますか?それとも仮想関数の呼び出しだけですか?また、仮想関数が実際に上書きされているかどうかに関係なく、速度が影響を受けますか、それとも仮想である限り、効果はありません。


2
Inside the C++ Object Modelによって傑作を読むことをお勧めしStanley B. Lippmanます。(セクション
4.2、124-131

回答:


123

仮想関数はどのようにして深いレベルで実装されますか?

「C ++の仮想関数」から:

プログラムで仮想関数が宣言されている場合は常に、クラスに対してv-tableが作成されます。vテーブルは、1つ以上の仮想関数を含むクラスの仮想関数へのアドレスで構成されます。仮想関数を含むクラスのオブジェクトには、メモリ内の仮想テーブルのベースアドレスを指す仮想ポインタが含まれています。仮想関数呼び出しがある場合は常に、vテーブルを使用して関数アドレスに解決されます。1つ以上の仮想関数を含むクラスのオブジェクトには、メモリ内のオブジェクトの先頭にvptrと呼ばれる仮想ポインターが含まれています。したがって、この場合のオブジェクトのサイズは、ポインタのサイズだけ増加します。このvptrには、メモリ内の仮想テーブルのベースアドレスが含まれています。仮想テーブルはクラス固有であることに注意してください。クラスに含まれる仮想関数の数に関係なく、クラスの仮想テーブルは1つだけです。この仮想テーブルには、クラスの1つ以上の仮想関数のベースアドレスが含まれています。オブジェクトで仮想関数が呼び出されるときに、そのオブジェクトのvptrは、メモリ内のそのクラスの仮想テーブルのベースアドレスを提供します。このテーブルには、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。そのオブジェクトのvptrは、メモリ内のそのクラスの仮想テーブルのベースアドレスを提供します。このテーブルには、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。そのオブジェクトのvptrは、メモリ内のそのクラスの仮想テーブルのベースアドレスを提供します。このテーブルには、そのクラスのすべての仮想関数のアドレスが含まれているため、関数呼び出しを解決するために使用されます。これは、仮想関数呼び出し中に動的バインディングが解決される方法です。

実行時にvtableを変更したり、直接アクセスしたりできますか?

普遍的には、答えは「ノー」だと思います。vtableを見つけるためにいくつかのメモリマングルを実行できますが、それを呼び出すには関数のシグネチャがどのように見えるかまだわかりません。この機能(言語がサポートする機能)で実現したいことは、vtableに直接アクセスしたり実行時に変更したりせずに可能でなければなりません。また、C ++言語仕様で、vtableが必要であると規定されていません。ただし、ほとんどのコンパイラが仮想関数を実装する方法です。

vtableはすべてのオブジェクトに存在しますか、それとも少なくとも1つの仮想関数を持つオブジェクトのみに存在しますか?

私は信じてここに答えはスペックが最初の場所でのvtableを必要としないので、「それは実装に依存」です。ただし、実際には、すべての最新のコンパイラは、クラスに少なくとも1つの仮想関数がある場合にのみvtableを作成すると思います。vtableに関連するスペースオーバーヘッドと、仮想関数と非仮想関数の呼び出しに関連する時間オーバーヘッドがあります。

抽象クラスは、少なくとも1つのエントリの関数ポインタにNULLを持っているだけですか?

答えは、言語仕様では規定されていないため、実装によって異なります。純粋仮想関数を呼び出すと、定義されていない場合(通常は定義されていません)、未定義の動作になります(ISO / IEC 14882:2003 10.4-2)。実際には、関数のvtableにスロットを割り当てますが、アドレスを割り当てません。これにより、vtableが不完全になり、派生クラスで関数を実装してvtableを完成させる必要があります。一部の実装では、vtableエントリにNULLポインタを配置するだけです。他の実装では、アサーションと同様の処理を行うダミーメソッドへのポインターを配置します。

抽象クラスは純粋な仮想関数の実装を定義できますが、その関数は修飾ID構文でのみ呼び出すことができます(つまり、メソッド名でクラスを完全に指定して、基本クラスメソッドを派生クラス)。これは、使いやすいデフォルト実装を提供するために行われますが、派生クラスはオーバーライドを提供する必要があります。

単一の仮想関数を使用すると、クラス全体の速度が低下しますか、それとも仮想関数の呼び出しだけが遅くなりますか?

これは私の知識の限界に近づいているので、私が間違っている場合は誰かがここで私を助けてください!

私は信じている時のパフォーマンスが対A非仮想関数仮想関数の呼び出しに関連するヒットクラスの経験に仮想された機能のみということ。クラスのスペースのオーバーヘッドはどちらの方法でもあります。vtableがある場合、オブジェクトごとに1つではなく、クラスごとに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


2
コンパイラが不要なvtableポインタを必要としないオブジェクトに配置することは、StroustrupのC ++の哲学と一致しません。ルールは、要求しない限り、Cにないオーバーヘッドが発生しないことであり、コンパイラがそれを破るのは失礼です。
スティーブジェソップ

3
私は、仮想関数が存在しないときにvtableを使用することを真剣に考えているコンパイラーにとってばかげたことだと思います。しかし、私の知る限りでは、C ++標準はそれを/ require /しないので、それに依存する前に警告されることを指摘することが重要だと感じました。
ザックバーリンゲーム、

8
仮想関数でさえ、非仮想的に呼び出すことができます。これは実際には非常に一般的です。オブジェクトがスタック上にある場合、スコープ内でコンパイラーは正確なタイプを認識し、vtableルックアップを最適化します。これは、同じスタックスコープで呼び出す必要があるdtorに特に当てはまります。
MSalters 2008

1
私は、少なくとも1つの仮想関数を持つクラスがすべてのオブジェクトにvtableを持ち、クラス全体に1つではないことを信じています。
Asaf R

3
一般的な実装:各オブジェクトにはvtableへのポインターがあります。クラスはテーブルを所有します。コンストラクションマジックは、ベースのctorが終了した後、派生したctor内のvtableポインターを更新するだけです。
MSalters 2008

31
  • 実行時にvtableを変更したり、直接アクセスしたりできますか?

移植性はありませんが、汚れたトリックを気にしない場合は、確認してください。

警告:この手法は、子供、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ビットを使用するプロセスでは、失敗する可能性があります。


6
うーん。これが賞金を受け取ったことは不吉に感じます。それが、@ Mobilewitsがそのような悪ふざけを実際に良い考えだと思っていることを意味しないことを願っています...
puetzk

1
「ウィンク」ではなく、このテクニックの使用を明確かつ強力に推奨しないことを検討してください。
einpoklum 2017

" vtblの内容は、単純にメンバーポインタの配列です。 "実際は、異なるエントリを持つレコード(構造体)であり、たまたま等間隔になっています
curiousguy

1
どちらの方法でも見ることができます。関数ポインターのシグネチャーは異なるため、ポインタータイプも異なります。その意味では、それは確かに構造のようなものです。しかし、他のコンテキストでは、vtblインデックスのアイデアは有用です(たとえば、ActiveXはタイプライブラリのデュアルインターフェイスを記述する方法でそれを使用します)。これは、より配列のようなビューです。
プエツク

17

単一の仮想関数を使用すると、クラス全体が遅くなりますか?

それとも仮想関数の呼び出しだけですか?また、仮想関数が実際に上書きされているかどうかに関係なく、速度が影響を受けますか、それとも仮想である限り、効果はありません。

そのようなクラスのオブジェクトを処理するときに、データの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のアドレスは、オブジェクト、適切なスロットから取得された関数ポインター、およびポインターによって呼び出された関数から読み取られます。実際のパフォーマンスに関しては、分岐予測が何らかの影響を与える可能性があります。したがって、たとえば、ほとんどのオブジェクトが特定の仮想関数の同じ実装を参照している場合、ポインターが取得される前でも、分岐予測器がどの関数を呼び出すかを正しく予測する可能性があります。ただし、どの関数が一般的なものであるかは関係ありません。ほとんどのオブジェクトは、上書きされていない基本ケースに委任されているか、ほとんどのオブジェクトが同じサブクラスに属しているため、同じ上書きされたケースに委任されている可能性があります。

深いレベルでどのように実装されていますか?

私はジェリコのアイデアがモック実装を使用してこれを実証するのが好きです。しかし、私はCを使用して上記のコードに似たものを実装し、低レベルがより見やすくなるようにします。

親クラスFoo

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!
}

派生クラスBar

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
}

仮想関数呼び出しを実行する関数f

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は、それが呼び出されたvtablebase.vtable、正しく型指定された式であるかに関係なく、常にオブジェクトのアドレスの最初の要素だからです。


「ポリモーフィッククラスのすべてのオブジェクトは、独自のvtableを指します。」すべてのオブジェクトに独自のvtableがあると言っていますか?AFAIK vtableは、同じクラスのすべてのオブジェクト間で共有されます。私が間違っているかどうか知らせてください。
Bhuwan

1
@ブーワン:いいえ、そうです。タイプごとに1つのvtableしかありません(テンプレートの場合、テンプレートのインスタンス化ごとにある可能性があります)。それに適用されるvtableを指すポリモーフィッククラスの各オブジェクトは、各オブジェクトにそのようなポインターがありますが、同じ型のオブジェクトの場合は同じテーブルを指すことを意味しました。おそらくこれを言い換える必要があります。
MvG

1
@MvG「同じタイプのオブジェクトは、同じテーブルをポイントします」仮想ベースクラスを使用したベースクラスの構築中ではありません!(非常に特殊なケース)
curiousguy

1
@curiousguy:特に仮想ベースの主なアプリケーションは多重継承であり、私がモデル化しなかったため、「上記は多くの点で簡略化されています」の下でファイルを提出します。しかし、コメントのおかげで、これをもっと深くする必要があるかもしれない人のためにここに置くと便利です。
MvG 2016


2

この回答はコミュニティWikiの回答に組み込まれました

  • 抽象クラスは、少なくとも1つのエントリの関数ポインタにNULLを持っているだけですか?

それに対する答えは、それが指定されていないことです-純粋な仮想関数を呼び出すと、それが定義されていない場合(通常は定義されていません)、未定義の動作になります(ISO / IEC 14882:2003 10.4-2)。一部の実装では、vtableエントリにNULLポインタを配置するだけです。他の実装では、アサーションと同様の処理を行うダミーメソッドへのポインターを配置します。

抽象クラスは純粋な仮想関数の実装を定義できますが、その関数は修飾ID構文でのみ呼び出すことができます(つまり、メソッド名でクラスを完全に指定して、基本クラスメソッドを派生クラス)。これは、使いやすいデフォルト実装を提供するために行われますが、派生クラスはオーバーライドを提供する必要があります。


また、抽象クラスが純粋な仮想関数の実装を定義できるとは思いません。定義により、純粋な仮想関数には本体がありません(例:bool my_func()= 0;)。ただし、通常の仮想関数の実装を提供できます。
ザックバーリンゲーム、

純粋仮想関数は定義を持つことができます。参照スコットマイヤーズ『効果的なC ++、第3版』アイテム#34、ISO 14882から2003 10.4-2、またはbytes.com/forum/thread572745.html
マイケル・バリ

2

クラスのメンバーとして関数ポインターを使用し、実装として静的関数を使用するか、実装のメンバー関数とメンバー関数へのポインターを使用して、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; }
...
};

void(*)(Foo*) MyFunc;これはいくつかのJava構文ですか?
curiousguy 2015

いいえ、関数ポインターのC / C ++構文。「関数ポインタを使用して、C ++で仮想関数の機能を再現できます」これは厄介な構文ですが、Cプログラマーであると考える場合は、よく知っておくべきことです。
jheriko

C関数ポインターは次のようになります。intPROC)(); また、クラスメンバー関数へのポインターは次のようになります。int(ClassName :: MPROC)();
Menace

1
@menace、そこで構文を忘れました... typedefを考えているのでしょうか?typedef int(* PROC)(); そのため、後でint(* foo)()の代わりにPROC fooを実行できますか?
jheriko 2015

2

私はそれを簡単にしようとします:)

私たちは皆、C ++の仮想関数を知っていますが、それらは深いレベルでどのように実装されますか?

これは、特定の仮想関数の実装である関数へのポインタを持つ配列です。この配列のインデックスは、クラスに定義された仮想関数の特定のインデックスを表します。これには、純粋な仮想関数が含まれます。

ポリモーフィッククラスが別のポリモーフィッククラスから派生する場合、次の状況が発生する可能性があります。

  • 派生クラスは、新しい仮想関数を追加したり、オーバーライドしたりしません。この場合、このクラスはvtableを基本クラスと共有します。
  • 派生クラスは、仮想メソッドを追加およびオーバーライドします。この場合、それは独自のvtableを取得します。追加された仮想関数は、最後に派生したものを超えて開始するインデックスを持っています。
  • 継承における複数のポリモーフィッククラス。この場合、2番目の基底と次の基底の間にインデックスシフトがあり、派生クラスにそのインデックスがあります。

実行時にvtableを変更したり、直接アクセスしたりできますか?

標準的な方法ではありません-それらにアクセスするためのAPIはありません。コンパイラには、それらにアクセスするためのいくつかの拡張機能またはプライベートAPIがある場合がありますが、それは拡張機能にすぎない場合があります。

vtableはすべてのクラスに存在しますか、それとも少なくとも1つの仮想関数を持つクラスのみに存在しますか?

少なくとも1つの仮想関数(それがデストラクタであっても)を持っているもの、またはvtableを持っている(「多形である」)少なくとも1つのクラスを派生しているもののみ。

抽象クラスは、少なくとも1つのエントリの関数ポインタにNULLを持っているだけですか?

これは可能な実装ですが、実際には行われていません。その代わり、通常、「呼び出された純粋な仮想関数」のようなものを出力して実行する関数があり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つのポインタを逆参照します。


2

以下は、最新のC ++での仮想テーブルの実行可能な手動実装です。明確に定義されたセマンティクスがあり、ハッキングやはありません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
}

1

各オブジェクトには、メンバー関数の配列を指すvtableポインターがあります。


1

これらすべての回答でここで言及されていないのは、基本継承クラスがすべて仮想メソッドを持つ多重継承の場合です。継承クラスには、vmtへの複数のポインターがあります。その結果、そのようなオブジェクトの各インスタンスのサイズは大きくなります。誰もが知っているように、仮想メソッドを持つクラスにはvmtのために4バイト余分にありますが、多重継承の場合は、仮想メソッドの4倍の基本クラスごとに4倍になります。4はポインターのサイズです。


0

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クラスに、Dによってオーバーライドされていない仮想メソッドfoo()とオーバーライドされている仮想メソッドbar()がある場合、Dのvtblには、Bのfoo()と独自のbar()へのポインターがあります。。B用に作成されたvtblはまだありません。


これは、2つの理由で正しくありません。1)抽象クラスに、純粋仮想メソッドに加えて通常の仮想メソッドが含まれる場合があり、2)純粋仮想メソッドに、完全修飾名で呼び出すことができる定義がオプションで含まれる場合があります。
マイケルバー

正解です。すべての仮想メソッドが純粋な仮想である場合、コンパイラーはvtableを最適化する可能性があると思います(定義がないことを確認するためにリンカーを形成するのに役立つ必要があります)。
マイケルバー

1
答えは、抽象クラスの仮想テーブルがまったく作成されないことです。不正解。「これらのクラスのオブジェクトを作成できないため、必要はありません!正解です。
curiousguy

のvtableはB 必要ないというあなたの論理的根拠を私はたどることができます。メソッドの一部に(デフォルト)実装があるからといって、vtableに格納する必要があるという意味ではありません。しかし、私はあなたのコードを実行しました(それをコンパイルするためのいくつかの修正を適用した)をgcc -S続けて実行しましたc++filt、そしてそこにB含まれるためのvtableが明らかにあります。vtableにはクラス名や継承などのRTTIデータも格納されているためと思われます。に必要な場合がありdynamic_cast<B*>ます。-fno-rttivtableが消えることもありません。のclang -O3代わりにgccそれは突然なくなっています。
MvG 2015

@MvG " メソッドの一部に(デフォルト)実装があるからといって、それらをvtableに格納する必要があるとは限りません "はい、それはそれだけを意味します。
curiousguy 2015

0

私が少し前に作成した非常にかわいい概念実証(継承の順序が重要かどうかを確認するため); 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ポインタのメモリを再利用します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.