27

コンパイラが仮想関数テーブルを作成するのはいつですか?

1) クラスに少なくとも 1 つの仮想関数が含まれている場合。

また

2) 直接の基本クラスに少なくとも 1 つの仮想関数が含まれている場合。

また

3) 階層の任意のレベルにある親クラスに少なくとも 1 つの仮想関数が含まれている場合。

これに関連する質問: C++ 階層で動的ディスパッチを放棄することは可能ですか?

たとえば、次の例を考えてみましょう。

#include <iostream>
using namespace std;
class A {
public:
  virtual void f();
};
class B: public A {
public:
  void f();
};
class C: public B {
public:
  void f();
};

V-Table を含むのはどのクラスですか?

B は f() を virtual として宣言していないので、クラス C は動的ポリモーフィズムを取得しますか?

4

6 に答える 6

24

「vtables は実装固有です」 (これはそうです) を超えて、vtable が使用されている場合: クラスごとに一意の vtables が存在します。B::fC::fは仮想と宣言されていませんが、基本クラス (コード内の A )からの仮想メソッドに一致するシグネチャがあるため、 B::fC::fは両方とも暗黙的に仮想です。 . 各クラスには少なくとも 1 つの一意の仮想メソッドがあるため ( B:: fはBインスタンスの A::fをオーバーライドし、 Cインスタンスの場合も同様にC:: f をオーバーライドします)、3 つの vtable が必要です。

通常、そのような詳細について心配する必要はありません。重要なのは、仮想ディスパッチがあるかどうかです。 呼び出す関数を明示的に指定することにより、仮想ディスパッチを使用する必要はありませんが、これは通常、仮想メソッドを実装する場合 (ベースのメソッドを呼び出す場合など) にのみ役立ちます。例:

struct B {
  virtual void f() {}
  virtual void g() {}
};

struct D : B {
  virtual void f() { // would be implicitly virtual even if not declared virtual
    B::f();
    // do D-specific stuff
  }
  virtual void g() {}
};

int main() {
  {
    B b; b.g(); b.B::g(); // both call B::g
  }
  {
    D d;
    B& b = d;
    b.g(); // calls D::g
    b.B::g(); // calls B::g

    b.D::g(); // not allowed
    d.D::g(); // calls D::g

    void (B::*p)() = &B::g;
    (b.*p)(); // calls D::g
    // calls through a function pointer always use virtual dispatch
    // (if the pointed-to function is virtual)
  }
  return 0;
}

役立つかもしれないいくつかの具体的なルール。ただし、これらについて私を引用しないでください。いくつかのエッジケースを見逃している可能性があります。

  • クラスに仮想メソッドまたは仮想ベースがある場合、継承されている場合でも、インスタンスには vtable ポインターが必要です。
  • クラスが継承されていない仮想メソッドを宣言する場合 (基本クラスがない場合など)、独自の vtable が必要です。
  • クラスに最初の基本クラスとは異なる一連のオーバーライド メソッドがある場合、そのクラスには独自の vtable が必要であり、基本の vtable を再利用することはできません。(デストラクタは通常これを必要とします。)
  • クラスに複数の基底クラスがあり、2 番目以降の基底に仮想メソッドがある場合:
    • 以前のベースに仮想メソッドがなく、空のベースの最適化がすべての以前のベースに適用された場合、このベースを最初のベース クラスとして扱います。
    • それ以外の場合、クラスには独自の vtable が必要です。
  • クラスに仮想基本クラスがある場合は、独自の vtable が必要です。

vtable はクラスの静的データ メンバーに似ており、インスタンスにはこれらへのポインターしかないことに注意してください。

Jan Gray による包括的な記事C++: Under the Hood (1994 年 3 月) も参照してください。(そのリンクが切れている場合は、Google を試してください。)

vtable を再利用する例:

struct B {
  virtual void f();
};
struct D : B {
  // does not override B::f
  // does not have other virtuals of its own
  void g(); // still might have its own non-virtuals
  int n; // and data members
};

特に、Bの dtor は仮想ではないことに注意してください (これは実際のコードでは間違いである可能性があります)。ただし、この例では、DインスタンスはBインスタンスと同じ vtable を指します。

于 2009-12-26T18:22:52.353 に答える
8

答えは、「場合による」です。それは、「vtbl を含む」という意味に依存し、特定のコンパイラの実装者が下した決定に依存します。

厳密に言えば、「クラス」に仮想関数テーブルが含まれることはありません。一部のクラスの一部のインスタンスには、仮想関数テーブルへのポインターが含まれています。ただし、これはセマンティクスの可能な実装の 1 つにすぎません。

極端な場合、コンパイラは、適切な仮想関数インスタンスを選択するために使用されるデータ構造にインデックス付けされたインスタンスに、仮想的に一意の番号を入れることができます。

「GCC は何をするのですか?」と尋ねたら、または「Visual C++ は何をしますか?」そうすれば、具体的な答えを得ることができます。

@Hassan Syedの答えはおそらくあなたが求めていたものに近いですが、ここで概念をまっすぐにしておくことが本当に重要です.

動作(新しく作成されたクラスに基づく動的ディスパッチ) と実装があります。あなたの質問は実装用語を使用していましたが、行動的な答えを探していたのではないかと思います。

動作上の答えは次のとおりです。仮想関数を宣言または継承するクラスは、その関数の呼び出し時に動的な動作を示します。そうでないクラスはそうしません。

実装に関しては、コンパイラはその結果を達成するために必要なことは何でも行うことができます。

于 2009-12-26T18:08:45.580 に答える
7

答え

クラス宣言に仮想関数が含まれている場合、vtable が作成されます。vtable は、親 (階層内のどこか) に仮想関数がある場合に導入されます。この親を Y と呼びましょう。Y の親には vtable はありません (virtual階層に他の関数がある場合を除きます)。

議論とテストのために読み進めてください

- 説明 -

メンバー関数を仮想として指定すると、実行時に基本クラスを介してサブクラスをポリモーフィックに使用しようとする可能性があります。言語設計に対する C++ のパフォーマンス保証を維持するために、C++ は可能な限り軽い実装戦略を提供しました。つまり、実行時にクラスがポリモーフィックに使用される可能性がある場合にのみ、1 レベルの間接化であり、プログラマーは少なくとも 1 つの関数をバーチャル。

virtual キーワードを使用しない場合、vtable のコストは発生しません。

-- edit : 編集内容を反映 --

基本クラスに仮想関数が含まれている場合にのみ、他のサブクラスに vtable が含まれます。上記の基本クラスの親には vtable がありません。

あなたの例では、3 つのクラスすべてに vtable があります。これは、3 つのクラスすべてをA*.

--テスト - GCC 4+ --

#include <iostream>

class test_base
{
  public:
    void x(){std::cout << "test_base" << "\n"; };
};

class test_sub : public test_base
{
public:
  virtual void x(){std::cout << "test_sub" << "\n"; } ;
};

class test_subby : public test_sub
{
public:
  void x() { std::cout << "test_subby" << "\n"; }
};

int main() 
{
  test_sub sub;
  test_base base;
  test_subby subby;

  test_sub * psub;
  test_base *pbase;
  test_subby * psubby;

  pbase = &sub;
  pbase->x();
  psub = &subby;
  psub->x();

  return 0;
}

出力

test_base
test_subby

test_baseには仮想テーブルがないため、それにキャストされたものはすべてx()fromを使用しtest_baseます。test_sub一方、 の性質を変更し、x()そのポインターは vtable を介して間接的に変更されます。これは、実行中test_subbyのによって示されx()ます。

そのため、vtable は、キーワード virtual が使用されている場合にのみ階層に導入されます。古い祖先には vtable がなく、ダウンキャストが発生すると、祖先の関数に組み込まれます。

于 2009-12-26T18:11:29.083 に答える
3

質問を非常に明確かつ正確にするために努力しましたが、まだ少し情報が不足しています。V-Table を使用する実装では、通常、テーブル自体は独立したデータ構造であり、ポリモーフィック オブジェクトの外部に格納されますが、オブジェクト自体はテーブルへの暗黙的なポインターのみを格納します。それで、あなたは何について尋ねていますか?になり得る:

  • オブジェクトに挿入された V-Table への暗黙的なポインタをオブジェクトが取得するのはいつですか?

また

  • 階層内の特定のタイプに対して専用の個別のV-Table が作成されるのはいつですか?

最初の質問に対する答えは次のとおりです。オブジェクトがポリモーフィック クラス型である場合、オブジェクトは V-Table への暗黙的なポインタを挿入されます。クラス タイプに少なくとも 1 つの仮想関数が含まれている場合、またはその直接または間接の親のいずれかがポリモーフィックである場合、クラス タイプはポリモーフィックです (これはセットからの回答 3 です)。また、複数の継承の場合、オブジェクトに複数の V-Table ポインターが埋め込まれてしまう可能性があることにも注意してください。

2 番目の質問に対する答えは、1 つ目の質問 (オプション 3) と同じになる可能性がありますが、例外がある可能性があります。単一継承階層内のポリモーフィック クラスに独自の仮想関数がない場合 (新しい仮想関数がなく、親仮想関数のオーバーライドがない場合)、実装がこのクラスの個別の V テーブルを作成しないことを決定する可能性がありますが、代わりにこのクラスにも直接の親の V-Table を使用します (とにかく同じになるため)。つまり、この場合、親型のオブジェクトと派生型のオブジェクトの両方が、埋め込まれた V-Table ポインターに同じ値を格納します。もちろん、これは実装に大きく依存します。GCC と MS VS 2005 を確認しましたが、そのようには動作しません。この状況では、どちらも派生クラスの個別の V-Table を作成します。

于 2009-12-26T18:58:29.270 に答える
2

C++ 標準では、V テーブルを使用してポリモーフィック クラスの錯覚を作成することは義務付けられていません。ほとんどの場合、実装では V-Table を使用して必要な追加情報を保存します。つまり、これらの追加情報は、少なくとも 1 つの仮想機能がある場合に装備されます。

于 2009-12-26T18:15:11.950 に答える
1

この動作は、C++ 言語仕様の 10.3 章、パラグラフ 2 で定義されています。

仮想メンバー関数 vf がクラス Base およびクラス Derived で宣言され、Base から直接的または間接的に派生した場合、Base::vf と同じ名前と同じパラメーター リストを持つメンバー関数 vf が宣言され、Derived::vf仮想でもあり (そう宣言されているかどうかに関係なく)、Base::vf をオーバーライドします。

A 関連する語句をイタリック体にしました。したがって、コンパイラが通常の意味で v-table を作成する場合、すべての f() メソッドが仮想であるため、すべてのクラスが v-table を持つことになります。

于 2009-12-26T19:01:30.467 に答える