質問

C++ の仮想関数が何であるかは誰もが知っていますが、深いレベルではどのように実装されるのでしょうか?

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

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

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

単一の仮想関数があるとクラス全体の速度が低下しますか?それとも仮想関数の呼び出しだけでしょうか?また、仮想関数が実際に上書きされるか否かによって速度は影響を受けますか、それとも仮想関数である限り影響はありません。

役に立ちましたか?

解決

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

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

プログラムで仮想関数が宣言されている場合は常に、そのクラスの v - テーブルが構築されます。v テーブルは、1 つ以上の仮想関数を含むクラスの仮想関数へのアドレスで構成されます。仮想関数を含むクラスのオブジェクトには、メモリ内の仮想テーブルのベース アドレスを指す仮想ポインタが含まれています。仮想関数呼び出しがあるときは常に、v-table を使用して関数アドレスが解決されます。1 つ以上の仮想関数を含むクラスのオブジェクトには、メモリ内のオブジェクトの先頭に vptr と呼ばれる仮想ポインタが含まれています。したがって、この場合のオブジェクトのサイズは、ポインタのサイズだけ増加します。この vptr には、メモリ内の仮想テーブルのベース アドレスが含まれます。仮想テーブルはクラス固有であることに注意してください。つまり、クラスに含まれる仮想関数の数に関係なく、クラスには仮想テーブルが 1 つだけ存在します。この仮想テーブルには、クラスの 1 つ以上の仮想関数のベース アドレスが含まれます。オブジェクト上で仮想関数が呼び出されるとき、そのオブジェクトの 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 構文 (つまり、メソッド名でクラスを完全に指定する、つまり、メソッドから基本クラスのメソッドを呼び出すのと同様) でのみ呼び出すことができることに注意してください。派生クラス)。これは、使いやすいデフォルト実装を提供するために行われますが、派生クラスがオーバーライドを提供する必要があります。

仮想関数が 1 つあるとクラス全体が遅くなりますか、それとも仮想関数の呼び出しだけが遅くなりますか?

これは私の知識の限界に達しているので、私が間違っていたら誰かがここで助けてください!

信じる クラス内の仮想関数のみが、仮想関数の呼び出しに関連する時間パフォーマンスのヒットを経験することと、非仮想関数。どちらの場合でも、クラスのオーバーヘッド領域が存在します。vtable がある場合、vtable ごとに 1 つだけ存在することに注意してください。 クラス, 、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

他のヒント

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

移植性はありませんが、汚いトリックを気にしないなら、もちろんです。

警告:このテクニックは、子供や 1 歳未満の大人による使用はお勧めできません。 969, 、またはアルファ・ケンタウリの小さな毛むくじゃらの生き物。副作用には次のものがあります。 鼻から飛び出す悪魔, 、突然の出現 ヨグ=ソトース 後続のすべてのコードレビューで必須の承認者として、または遡及的に追加 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 ビットを使用するプロセスでは、失敗する可能性があります。

単一の仮想関数があるとクラス全体の速度が低下しますか?

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

仮想関数を使用すると、そのようなクラスのオブジェクトを処理するときにデータのもう 1 つの項目を初期化、コピーする必要があるため、クラス全体の速度が低下します。メンバーが 6 人程度のクラスの場合、その差はごくわずかであるはずです。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 のアドレスはオブジェクトから読み取られ、関数ポインターは適切なスロットから取得され、関数はポインターによって呼び出されます。実際のパフォーマンスに関しては、分岐予測が何らかの影響を与える可能性があります。したがって、たとえば、ほとんどのオブジェクトが特定の仮想関数の同じ実装を参照している場合、ポインターが取得される前であっても、分岐予測機能がどの関数を呼び出すかを正確に予測する可能性があります。ただし、どの関数が一般的な関数であるかは重要ではありません。それは、ほとんどのオブジェクトが上書きされていない基本ケースに委譲することも、ほとんどのオブジェクトが同じサブクラスに属しているため、同じ上書きケースに委譲することもできます。

深いレベルではどのように実装されているのでしょうか?

私は、模擬実装を使用してこれを実証する jheriko のアイデアが好きです。ただし、低レベルをより簡単に確認できるように、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 呼び出されるかどうかに関係なく、常にオブジェクトのアドレスの最初の要素になります。 vtable または base.vtable 正しく入力された式で。

通常は、関数へのポインターの配列である VTable を使用します。

この回答は コミュニティ Wiki の回答

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

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

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

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

簡単にしてみます:)

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 つのポインターを逆参照します。

がここにあります 実行可能 最新の 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
}

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

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

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

少し前に作成した非常にかわいい概念実証 (継承の順序が重要かどうかを確認するため)。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 ポインターのメモリを再利用するために、動的に割り当てられたオブジェクトのスコープの最後に自動的に配置されます。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top