虚拟函数和vtable如何实现?


109

我们都知道C ++中有什么虚函数,但是如何在深层次上实现它们呢?

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

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

对于至少一个条目的函数指针,抽象类是否仅具有NULL?

拥有一个虚拟函数会减慢整个班级吗?还是仅调用虚拟函数?速度是否会受到影响(无论是否实际覆盖了虚函数),或者只要它是虚函数,速度就不会起作用。


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

Answers:


123

虚拟功能如何在深层次上实现?

来自“ C ++中的虚函数”

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

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

我普遍认为答案是“否”。您可以进行一些内存修改以找到vtable,但是您仍然不知道函数签名的外观。无需直接访问vtable或在运行时对其进行修改,就可以使用此功能(该语言支持)实现任何目标。还要注意,C ++语言规范指定需要vtable,但是大多数编译器都是通过这种方式实现虚函数的。

该vtable是否对所有对象都存在,或者仅对于具有至少一个虚函数的对象存在?

认为这里的答案是“取决于实现”,因为该规范首先不需要vtables。但是,实际上,我相信所有现代编译器仅在一个类至少具有1个虚函数的情况下才创建vtable。与vtable相关联的空间开销与与调用虚拟函数与非虚拟函数相关联的时间开销。

对于至少一个条目的函数指针,抽象类是否仅具有NULL?

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

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

具有单个虚拟函数会减慢整个类的速度,还是仅降低对虚拟函数的调用?

这已成为我所学的知识,所以如果我错了,请有人在这里帮助我!

相信只有类中的虚拟函数会遇到与调用虚拟函数和非虚拟函数有关的时间性能损失。该类的空间开销是任意一种。请注意,如果有一个vtable,则每个只有一个,而每个对象只有一个。

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

我不相信被覆盖的虚函数的执行时间与调用基本虚函数相比会减少。但是,与为派生类和基类定义另一个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 /它,因此请在使用前警告它。
Zach Burlingame

8
甚至虚函数也可以非虚调用。这实际上很常见:如果对象在堆栈上,则在范围内,编译器将知道确切的类型并优化vtable查找。对于dtor尤其如此,必须在同一堆栈范围内调用它。
MSalters

1
我相信,当一个类至少具有一个虚函数时,每个对象都有一个vtable,而不是整个类中的一个。
Asaf R

3
通用实现:每个对象都有一个指向vtable的指针;该类拥有该表。基本构造器完成后,构造魔术仅包括更新派生的构造器中的vtable指针。
MSalters

31
  • 是否可以在运行时修改vtable甚至直接访问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!

为所有实例替换方法(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位的过程中,它很可能会失败。


6
嗯 收到赏金令人不祥。我希望这并不意味着@Mobilewits认为这样的恶作剧实际上是一个好主意……
puetzk 2015年

1
请考虑清楚,强烈地鼓励不要使用此技术,而不要“眨眼”。
einpoklum

vtbl内容只是成员指针的数组 ”实际上,它是具有不同条目的记录(结构),碰巧是均匀分布的
curiousguy

1
您可以用任何一种方式查看它。函数指针具有不同的签名,因此具有不同的指针类型;从这个意义上讲,它确实是结构化的。但是在其他情况下,vtbl索引的概念很有用(例如,ActiveX以它在typelibs中描述双重接口的方式使用它),这是一个更类似于数组的视图。
puetzk

17

拥有一个虚拟函数会减慢整个班级吗?

还是仅调用虚拟函数?速度是否会受到影响(无论是否实际覆盖了虚函数),或者只要它是虚函数,速度就不会起作用。

拥有虚拟函数会减慢整个类的速度,因为在处理此类对象时,必须初始化,复制更多数据……。对于只有六个成员左右的班级,差异应该忽略不计。对于仅包含一个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。” 您是说每个对象都有自己的vtable吗?AFAIK vtable在同一类的所有对象之间共享。让我知道我是否错。
不丹

1
@Bhuwan:不,您是对的:每种类型只有一个vtable(如果是模板,则可能是每个模板实例化)。我的意思是说,一个多态类的每个对象都指向适用于它的vtable,因此每个对象都有这样的指针,但是对于相同类型的对象,它将指向同一张表。也许我应该改写这个词。
MvG

1
@MvG“ 相同类型的对象将指向同一表 ”,而不是在使用虚拟基类构造基类的过程中!(一个非常特殊的情况)
curiousguy

1
@curiousguy:我想在“以上内容在许多方面进行了简化”下提出,尤其是因为虚拟基础的主要应用是多重继承,我也没有建模。但是,感谢您的评论,对于可能需要更深入了解的人来说,将其放在此处很有用。
MvG


2

该答案已合并到社区Wiki答案中

  • 对于至少一个条目的函数指针,抽象类是否仅具有NULL?

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

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


另外,我认为抽象类不能为纯虚函数定义实现。通过定义,纯虚函数没有主体(例如bool my_func()= 0;)。但是,您可以提供常规虚拟功能的实现。
Zach Burlingame

纯虚函数可以具有定义。参见Scott Meyers的“ Effective C ++,第三版”项目#34,ISO 14882-2003 10.4-2或bytes.com/forum/thread572745.html
Michael Burr

2

您可以使用函数指针作为类的成员并使用静态函数作为实现,或者使用指向成员函数和实现的成员函数的指针来重新创建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语法吗?
curiousguy 2015年

不,它的C / C ++语法用于函数指针。引用我自己的话:“您可以使用函数指针在C ++中重新创建虚拟函数的功能”。这是一个令人讨厌的语法,但是如果您认为自己是C程序员,则需要熟悉一下。
jheriko

ac函数指针看起来更像是:int(PROC)(); 和指向类成员函数的指针将类似于:int(ClassName :: MPROC)();
威胁

1
@menace,您在那里忘记了一些语法...您是否正在考虑typedef?typedef int(* PROC)(); 所以您可以稍后再做PROC foo而不是int(* foo)()吗?
jheriko

2

我将尝试使其变得简单:)

我们都知道C ++中有什么虚函数,但是如何在深层次上实现它们呢?

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

当一个多态类从另一个多态类派生时,我们可能会遇到以下情况:

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

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

非标准方式-没有API可以访问它们。编译器可能具有一些扩展程序或专用API来访问它们,但这仅是扩展程序。

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

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

对于至少一个条目的函数指针,抽象类是否仅具有NULL?

这是可能的实现,但是没有实践。取而代之的是,通常有一个函数会打印出类似“ pure virtual function named”的字样并执行abort()。如果尝试在构造函数或析构函数中调用abstract方法,则可能会发生对此的调用。

拥有一个虚拟函数会减慢整个班级吗?还是仅调用虚拟函数?速度是否会受到影响(无论是否实际覆盖了虚函数),或者只要它是虚函数,速度就不会起作用。

减速仅取决于将呼叫解析为直接呼叫还是虚拟呼叫。没什么关系。:)

如果您通过指针或对对象的引用来调用虚拟函数,则它将始终实现为虚拟调用-因为编译器永远无法在运行时知道将哪种对象分配给该指针,以及该对象是否属于对象。是否重写此方法的类。仅在两种情况下,编译器才能将对虚拟函数的调用解析为直接调用:

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

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


2

这是现代C ++中可运行的虚拟表手动实现。它具有定义明确的语义,没有hack,也没有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。


0

除以下问题外,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)可选地,纯虚拟方法可能具有可以使用完全限定名称调用的定义。
Michael Burr

正确-再次想想,如果所有虚拟方法都是纯虚拟方法,则编译器可能会将vtable优化掉(它需要链接器的帮助以确保也没有定义)。
Michael Burr

1
答案是根本没有为抽象类创建虚拟表。 ”错误。“ 没有必要,因为无法创建这些类的对象! ”错误。
curiousguy

我可以按照你的逻辑,即不虚表的B 应该是必要的。仅仅因为其某些方法具有(默认)实现,并不意味着它们必须存储在vtable中。但是,我只是先运行了您的代码(对一些修复程序进行了一些模块化修改,以使其编译),gcc -S然后在c++filt其中显然B包含了一个vtable 。我猜可能是因为vtable还存储了RTTI数据,如类名和继承。可能需要dynamic_cast<B*>。甚至-fno-rtti不能使vtable消失。用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指针的内存。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.