【问题标题】:How are virtual functions and vtable implemented?虚函数和vtable是如何实现的?
【发布时间】:2021-01-10 11:05:06
【问题描述】:

我们都知道 C++ 中的虚函数是什么,但它们是如何深入实现的呢?

可以在运行时修改甚至直接访问vtable吗?

vtable 是否适用于所有类,还是仅适用于至少具有一个虚函数的类?

抽象类是否只是简单地为至少一个条目的函数指针设置了一个 NULL 值?

只有一个虚函数会减慢整个班级的速度吗?还是只调用虚拟函数?如果虚函数实际上被覆盖,速度是否会受到影响,或者只要它是虚函数就没有影响。

【问题讨论】:

  • 建议阅读Inside the C++ Object Model by Stanley B. Lippman 的杰作。 (第 4.2 节,第 124-131 页)

标签: c++ polymorphism virtual-functions vtable


【解决方案1】:

通常使用 VTable,即指向函数的指针数组。

【讨论】:

    【解决方案2】:

    每个对象都有一个指向成员函数数组的 vtable 指针。

    【讨论】:

      【解决方案3】:

      虚函数是如何深层次实现的?

      来自"Virtual Functions in C++"

      只要程序声明了一个虚函数,就会为该类构造一个 v - 表。 v-table 由包含一个或多个虚函数的类的虚函数地址组成。包含虚函数的类的对象包含一个虚指针,它指向内存中虚表的基地址。每当有虚函数调用时,都会使用 v-table 解析到函数地址。包含一个或多个虚函数的类的对象在内存中对象的最开始处包含一个称为 vptr 的虚指针。因此,在这种情况下,对象的大小会随着指针的大小而增加。这个 vptr 包含内存中虚拟表的基地址。请注意,虚拟表是特定于类的,即一个类只有一个虚拟表,而与它包含的虚拟函数的数量无关。该虚拟表又包含该类的一个或多个虚拟函数的基地址。在对象上调用虚函数时,该对象的 vptr 提供了该类在内存中的虚拟表的基地址。该表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是在虚函数调用期间解决动态绑定的方式。

      可以在运行时修改甚至直接访问vtable吗?

      总的来说,我相信答案是“不”。你可以做一些内存修改来找到 vtable,但你仍然不知道函数签名是什么样子的。您想通过这种能力(语言支持)实现的任何事情都应该可以在不直接访问 vtable 或在运行时修改它的情况下实现。另请注意,C++ 语言规范没有指定 vtable 是必需的 - 但是大多数编译器都是这样实现虚函数的。

      vtable 是否存在于所有对象,还是只存在于具有至少一个虚函数的对象?

      相信这里的答案是“这取决于实现”,因为规范首先不需要 vtables。然而,在实践中,我相信所有现代编译器只有在一个类至少有 1 个虚函数时才会创建一个 vtable。存在与 vtable 相关的空间开销以及与调用虚函数与​​非虚函数相关的时间开销。

      抽象类是否简单地为至少一个条目的函数指针设置了一个 NULL?

      答案是语言规范未指定它,因此它取决于实现。如果未定义(通常未定义),则调用纯虚函数会导致未定义的行为(ISO/IEC 14882:2003 10.4-2)。实际上,它确实在 vtable 中为函数分配了一个槽,但没有为其分配地址。这使得 vtable 不完整,需要派生类来实现功能并完成 vtable。一些实现只是在 vtable 条目中放置一个 NULL 指针;其他实现会放置一个指向虚拟方法的指针,该方法执行类似于断言的操作。

      请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用限定 ID 语法调用(即,在方法名称中完全指定类,类似于调用基类派生类的方法)。这样做是为了提供易于使用的默认实现,同时仍需要派生类提供覆盖。

      只有一个虚函数会减慢整个类的速度还是只减慢对虚函数的调用?

      这是我知识的边缘,所以如果我错了,请有人帮助我!

      相信只有类中的虚函数会经历与调用虚函数与​​非虚函数相关的时间性能损失。无论哪种方式,该类的空间开销都是存在的。请注意,如果有 vtable,则每个 class 只有 1 个,而不是每个 object 一个。

      如果虚函数实际上被覆盖,速度是否会受到影响,或者只要它是虚函数就没有影响?

      我不相信与调用基本虚函数相比,被覆盖的虚函数的执行时间不会减少。但是,与为派生类和基类定义另一个 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 指针放在不需要它的对象中是不符合 Stroustrup 的 C++ 哲学的。规则是,除非您要求,否则您不会获得不在 C 中的开销,编译器破坏它是不礼貌的。
      • 我同意,当没有虚函数存在时,任何认真对待自己使用 vtable 的编译器都是愚蠢的。但是,我觉得有必要指出,据我所知,C++ 标准并不 / 要求 / 它,因此在依赖它之前请注意。
      • 甚至虚函数也可以非虚调用。这实际上很常见:如果对象在堆栈上,则在范围内编译器将知道确切的类型并优化 vtable 查找。对于 dtor 尤其如此,它必须在同一个堆栈范围内调用。
      • 我相信当一个类至少有一个虚函数时,每个对象都有一个vtable,而不是整个类都有一个。
      • 常见实现:每个对象都有一个指向vtable的指针;该类拥有该表。构造魔法只是在基 ctor 完成后更新派生 ctor 中的 vtable 指针。
      【解决方案4】:

      此答案已纳入Community Wiki answer

      • 抽象类是否只是为至少一个条目的函数指针设置了一个 NULL?

      答案是它是未指定的 - 如果未定义(通常未定义)调用纯虚函数会导致未定义的行为(ISO/IEC 14882:2003 10.4-2)。一些实现只是在 vtable 条目中放置一个 NULL 指针;其他实现会放置一个指向虚拟方法的指针,该方法执行类似于断言的操作。

      请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用限定 ID 语法调用(即,在方法名称中完全指定类,类似于调用基类派生类的方法)。这样做是为了提供易于使用的默认实现,同时仍需要派生类提供覆盖。

      【讨论】:

      • 另外,我不认为抽象类可以定义纯虚函数的实现。根据定义,纯虚函数没有主体(例如 bool my_func() = 0;)。但是,您可以提供常规虚函数的实现。
      • 纯虚函数可以有定义。请参阅 Scott Meyers 的“Effective C++,第 3 版”第 #34 条,ISO 14882-2003 10.4-2,或bytes.com/forum/thread572745.html
      【解决方案5】:

      Burly 的答案在这里是正确的,除了问题:

      抽象类是否简单地为至少一个条目的函数指针设置了一个 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。

      回应 Mike 在下面的评论...

      如果我描述的B类有一个没有被D覆盖的虚方法foo()和一个被覆盖的虚方法bar(),那么D的vtbl 将有一个指向 B 的 foo() 和它自己的 bar() 的指针。仍然没有为 B 创建 vtbl。

      【讨论】:

      • 这是不正确的,原因有两个:1) 除了纯虚方法之外,抽象类可能还有常规虚方法,以及 2) 纯虚方法可能有一个定义,可以用完全限定名称。
      • 对 - 再想一想,我想如果所有虚拟方法都是纯虚拟的,编译器可能会优化 vtable(它需要帮助形成链接器以确保也没有定义)。跨度>
      • "答案是根本没有为抽象类创建虚拟表。" 错误。 “没有必要,因为无法创建这些类的对象!”错误。
      • 我可以按照你的理由认为B 应该不需要 vtable。仅仅因为它的一些方法具有(默认)实现并不意味着它们必须存储在 vtable 中。但我只是通过gcc -S 运行您的代码(模数一些修复以使其编译),然后是c++filt,显然其中包含B 的vtable。我猜这可能是因为 vtable 还存储 RTTI 数据,如类名和继承。 dynamic_cast<B*> 可能需要它。即使-fno-rtti 也不会使 vtable 消失。使用 clang -O3 而不是 gcc 它突然消失了。
      • @MvG "仅仅因为它的某些方法具有(默认)实现并不意味着它们必须存储在 vtable 中" 是的,就是这样。
      【解决方案6】:
      • 可以在运行时修改甚至直接访问 vtable 吗?

      不便携,但如果你不介意肮脏的把戏,当然可以!

      警告:不建议儿童、969 岁以下的成年人或来自 Alpha Centauri 的毛茸茸的小动物使用此技术。副作用可能包括demons which fly out of your noseYog-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!
      

      为所有实例替换一个方法(monkeypatching 一个类)

      这个有点棘手,因为 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 位的进程中,它很可能会失败。

      【讨论】:

      • 嗯。收到赏金感觉不祥。我希望这并不意味着@Mobilewits 认为这样的恶作剧实际上是个好主意......
      • 请考虑清楚而强烈地劝阻使用这种技术,而不是“眨眼”。
      • "vtbl 内容只是一个成员指针数组" 实际上它是一个具有不同条目的记录(结构),恰好间隔均匀
      • 你可以看看它;函数指针具有不同的签名,因此指针类型也不同;从这个意义上说,它确实是类似结构的。但在其他情况下,vtbl 索引的想法是有用的(例如 ActiveX 以它在类型库中描述双接口的方式使用它),这是一个更像数组的视图。
      【解决方案7】:

      您可以在 C++ 中重新创建虚函数的功能,使用函数指针作为类的成员,静态函数作为实现,或者使用指向成员函数和成员函数的指针作为实现。这两种方法之间只有符号上的优势……事实上,虚函数调用本身只是一种符号上的便利。事实上,继承只是一种符号上的便利……它都可以在不使用继承的语言特性的情况下实现。 :)

      以下是未经测试的垃圾代码,可能有错误的代码,但希望能证明这个想法。

      例如

      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 语法吗?
      • 不,它的 C/C++ 函数指针语法。引用我自己的话“您可以使用函数指针在 C++ 中重新创建虚函数的功能”。这是一个令人讨厌的语法,但如果你认为自己是 C 程序员,你应该熟悉一些东西。
      • c 函数指针看起来更像: int (PROC)();指向类成员函数的指针看起来像: int (ClassName:: MPROC)();
      • @menace,你忘记了那里的一些语法......你可能在想 typedef 吗? typedef int(*PROC)();所以你可以稍后再做 PROC foo 而不是 int(*foo)() ?
      【解决方案8】:

      只有一个虚函数会减慢整个班级的速度吗?

      或者只是对虚拟函数的调用?如果虚函数实际上被覆盖,速度是否会受到影响,或者只要它是虚函数就没有影响。

      拥有虚函数会减慢整个类的速度,因为在处理此类对象时,必须再初始化、复制一项数据……。对于一个有六个左右成员的班级,差异应该可以忽略不计。对于仅包含单个 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 来检查一个类是什么类型。上面的内容在很多方面都得到了简化,例如多重继承,但一般概念是合理的。

      如果argFoo* 类型,而您采用arg->vtable,但实际上是Bar 类型的对象,那么您仍然会得到vtable 的正确地址。这是因为 vtable 始终是对象地址的第一个元素,无论它在正确类型的表达式中称为 vtable 还是 base.vtable

      【讨论】:

      • "多态类的每个对象都将指向它自己的 vtable。"您是说每个对象都有自己的 vtable 吗? AFAIK vtable 在同一类的所有对象之间共享。如果我错了,请告诉我。
      • @Bhuwan:不,你是对的:每种类型只有一个 vtable(在模板的情况下可能是每个模板实例化)。我的意思是说,多态类的每个对象都指向适用于它的 vtable,因此每个对象都有这样的指针,但对于相同类型的对象,它将指向同一个表。也许我应该改写这个。
      • @MvG "相同类型的对象将指向同一个表" 不在使用虚拟基类构建基类期间! (非常特殊的情况)
      • @curiousguy:我会在“上面在很多方面进行了简化”下归档,特别是因为虚拟基的主要应用是多重继承,我也没有建模。但是感谢您的评论,对于可能需要更多深度的人来说,这里有这个很有用。
      【解决方案9】:

      在所有这些答案中没有提到的一点是,在多重继承的情况下,基类都具有虚拟方法。继承类有多个指向 vmt 的指针。 结果是这样一个对象的每个实例的大小都更大。 每个人都知道带有虚方法的类有 4 个额外的 vmt 字节,但在多重继承的情况下,对于每个具有虚方法的基类来说,都是 4。4 是指针的大小。

      【讨论】:

        【解决方案10】:

        我会尽量简化:)

        我们都知道 C++ 中的虚函数是什么,但它们是如何深入实现的呢?

        这是一个带有函数指针的数组,函数是特定虚函数的实现。此数组中的索引表示为类定义的虚函数的特定索引。这包括纯虚函数。

        当一个多态类派生自另一个多态类时,我们可能有以下几种情况:

        • 派生类不会添加新的虚函数,也不会覆盖任何虚函数。在这种情况下,此类与基类共享 vtable。
        • 派生类添加和覆盖虚拟方法。在这种情况下,它会获得自己的 vtable,其中添加的虚函数的索引从最后一个派生函数开始。
        • 继承中有多个多态类。在这种情况下,我们在第二个和下一个基数之间有一个索引移位,以及它在派生类中的索引

        可以在运行时修改甚至直接访问vtable吗?

        不是标准方式 - 没有 API 可以访问它们。编译器可能有一些扩展或私有 API 来访问它们,但这可能只是一个扩展。

        vtable 是否适用于所有类,还是仅适用于至少具有一个虚函数的类?

        仅具有至少一个虚函数(甚至是析构函数)或派生至少一个具有其 vtable 的类(“是多态的”)。

        抽象类是否简单地为至少一个条目的函数指针设置了一个 NULL?

        这是一种可能的实现方式,但尚未实践。相反,通常有一个函数会打印类似“调用的纯虚函数”并执行abort()。如果您尝试在构造函数或析构函数中调用抽象方法,则可能会调用它。

        只有一个虚函数会减慢整个班级的速度吗?还是只调用虚拟函数?如果虚函数实际上被覆盖,速度是否会受到影响,或者只要它是虚函数就没有影响。

        减速仅取决于呼叫是直接呼叫还是虚拟呼叫。其他都不重要。 :)

        如果你通过一个指针或一个对象的引用来调用一个虚函数,那么它总是会被实现为虚调用——因为编译器永远无法知道在运行时会给这个指针分配什么样的对象,以及它是否是这个方法是否被覆盖的类。只有在两种情况下,编译器才能将对虚函数的调用解析为直接调用:

        • 如果您通过值(返回值的变量或函数的结果)调用方法 - 在这种情况下,编译器不会怀疑对象的实际类是什么,并且可以“硬解析”它在编译时。
        • 如果虚拟方法在类中声明为final,您可以通过该指针或引用调用它(仅在 C++11 中)。在这种情况下,编译器知道这个方法不能被进一步覆盖,它只能是这个类的方法。

        请注意,尽管虚拟调用仅具有取消引用两个指针的开销。使用 RTTI(尽管仅适用于多态类)比调用虚拟方法要慢,如果您找到一种以两种方式实现相同事物的案例。例如,定义virtual bool HasHoof() { return false; } 然后仅将其覆盖为bool Horse::HasHoof() { return true; } 将使您能够调用if (anim->HasHoof()),这将比尝试if(dynamic_cast<Horse*>(anim)) 更快。这是因为dynamic_cast 在某些情况下必须遍历类层次结构,甚至递归地查看是否可以从实际指针类型和所需的类类型构建路径。虽然虚拟调用始终相同 - 取消引用两个指针。

        【讨论】:

          【解决方案11】:

          我早些时候做了非常可爱的概念证明(看看继承顺序是否重要);让我知道你的 C++ 实现是否真的拒绝它(我的 gcc 版本只给出分配匿名结构的警告,但这是一个错误),我很好奇。

          CPolite.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 */
          

          CPolite_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
          

          ma​​in.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 指针的内存。

          【讨论】:

            【解决方案12】:

            这是现代 C++ 中虚拟表的可运行手动实现。它具有明确定义的语义,没有 hack 也没有 void*

            注意:.*-&gt;* 是不同于 *-&gt; 的运算符。成员函数指针的工作方式不同。

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

            【讨论】:

              猜你喜欢
              • 2010-09-11
              • 2012-04-21
              • 2011-07-22
              • 2017-05-31
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多