编译器如何知道vtable中的哪个条目对应于虚函数?

时间:2015-10-08 02:57:24

标签: c++ compiler-construction virtual-functions vtable

假设我们在父类和派生类中有多个虚函数。对于父派生类,将在vtable中为这些虚函数创建一个vtable。

编译器如何知道vtable中的哪个条目对应哪个虚函数?

示例:

class Animal{
public:
 void fakeMethod1(){}
 virtual void getWeight(){}
 void fakeMethod2(){}
 virtual void getHeight(){}
 virtual void getType(){}
};

class Tiger:public Animal{
public:
 void fakeMethod3(){}
 virtual void getWeight(){}
 void fakeMethod4(){}
 virtual void getHeight(){}
 virtual void getType(){}
};
main(){
Animal a* = new Tiger();
a->getHeight(); // A  will now point to the base address of vtable Tiger
//How will the compiler know which entry in the vtable corresponds to the function getHeight()?
}

我的研究中没有找到确切的解释 -

https://stackoverflow.com/a/99341/437894 =

  

“此表用于解析函数调用,因为它包含   该类的所有虚函数的地址。“

用于解析函数调用的表格究竟是什么?

https://stackoverflow.com/a/203136/437894 =

  

“因此在运行时,代码只使用对象的vptr来定位   vtbl,并从那里得到实际重写函数的地址。“

我无法理解这一点。 Vtable保存虚函数的地址而不是实际重写函数的地址。

2 个答案:

答案 0 :(得分:8)

我会稍微修改一下你的例子,这样就会显示出面向对象的更多有趣方面。

假设我们有以下内容:

#include <iostream>

struct Animal
{
  int age;
  Animal(int a) : age {a} {}
  virtual int setAge(int);
  virtual void sayHello() const;
};

int
Animal::setAge(int a)
{
  int prev = this->age;
  this->age = a;
  return prev;
}

void
Animal::sayHello() const
{
  std::cout << "Hello, I'm an " << this->age << " year old animal.\n";
}

struct Tiger : Animal
{
  int stripes;
  Tiger(int a, int s) : Animal {a}, stripes {s} {}
  virtual void sayHello() const override;
  virtual void doTigerishThing();
};

void
Tiger::sayHello() const
{
  std::cout << "Hello, I'm a " << this->age << " year old tiger with "
            << this->stripes << " stripes.\n";
}

void
Tiger::doTigerishThing()
{
  this->stripes += 1;
}


int
main()
{
  Tiger * tp = new Tiger {7, 42};
  Animal * ap = tp;
  tp->sayHello();         // call overridden function via derived pointer
  tp->doTigerishThing();  // call child function via derived pointer
  tp->setAge(8);          // call parent function via derived pointer
  ap->sayHello();         // call overridden function via base pointer
}

我忽略了一个好的建议,即virtual函数成员的类应该有一个virtual析构函数用于此示例。无论如何,我还要泄漏物体。

让我们看看我们如何将这个例子转化为没有成员函数的好老C,单独留下virtual个。以下所有代码都是C,而不是C ++。

struct animal很简单:

struct animal
{
  const void * vptr;
  int age;
};

age成员外,我们还添加了一个vptr,它将成为vtable的指针。我之所以使用void指针是因为我们无论如何都必须进行丑陋的演员表,使用void *可以减少丑陋。

接下来,我们可以实现成员函数。

static int
animal_set_age(void * p, int a)
{
  struct animal * this = (struct animal *) p;
  int prev = this->age;
  this->age = a;
  return prev;
}

注意附加的第0个参数:在C ++中隐式传递的this指针。我再次使用void *指针,因为它将在以后简化操作。注意里面任何成员函数,我们总是静态知道 this指针的类型,所以演员没有问题。 (在机器级别,它根本不做任何事情。)

同样定义sayHello成员,但这次this指针符合const条件。

static void
animal_say_hello(const void * p)
{
  const struct animal * this = (const struct animal *) p;
  printf("Hello, I'm an %d year old animal.\n", this->age);
}

动物vtable的时间。首先,我们必须给它一个类型,这是直截了当的。

struct animal_vtable_type
{
  int (*setAge)(void *, int);
  void (*sayHello)(const void *);
};

然后我们创建vtable的单个实例并使用正确的成员函数进行设置。如果Animal拥有纯virtual成员,则相应的条目将具有NULL值,最好不要取消引用。

static const struct animal_vtable_type animal_vtable = {
  .setAge = animal_set_age,
  .sayHello = animal_say_hello,
};

请注意,animal_set_ageanimal_say_hello已声明为static。这是onkay,因为它们永远不会被引用,而只能通过vtable引用(仅通过vptr引用vtable,因此它也可以static

我们现在可以实现Animal ...

的构造函数
void
animal_ctor(void * p, int age)
{
  struct animal * this = (struct animal *) p;
  this->vptr = &animal_vtable;
  this->age = age;
}

...以及相应的operator new

void *
animal_new(int age)
{
  void * p = malloc(sizeof(struct animal));
  if (p != NULL)
    animal_ctor(p, age);
  return p;
}

关于唯一有趣的是在构造函数中设置vptr的行。

让我们继续老虎。

Tiger继承自Animal,因此会获得struct tiger个子对象。我通过放置struct animal作为第一个成员来做到这一点。至关重要的是,这是第一个成员,因为它意味着该对象的第一个成员 - vptr - 与我们的对象具有相同的地址。当我们做一些棘手的演员时,我们以后会需要这个。

struct tiger
{
  struct animal base;
  int stripes;
};

我们也可以在struct animal定义的开头简单地复制struct tiger成员,但这可能更难维护。编译器并不关心这些风格问题。

我们已经知道如何实现老虎的成员函数。

void
tiger_say_hello(const void * p)
{
  const struct tiger * this = (const struct tiger *) p;
  printf("Hello, I'm an %d year old tiger with %d stripes.\n",
         this->base.age, this->stripes);
}

void
tiger_do_tigerish_thing(void * p)
{
  struct tiger * this = (struct tiger *) p;
  this->stripes += 1;
}

请注意,这次我们将this指针投射到struct tiger。如果调用tiger函数,this指针最好指向老虎,即使我们是通过基指针调用的。

vtable旁边:

struct tiger_vtable_type
{
  int (*setAge)(void *, int);
  void (*sayHello)(const void *);
  void (*doTigerishThing)(void *);
};

请注意,前两个成员与animal_vtable_type完全相同。这是必不可少的,基本上是你问题的直接答案。如果我放置struct animal_vtable_type作为第一个成员,那可能会更明确。我想强调的是,对象布局应该完全相同,除非在这种情况下我们无法发挥我们讨厌的演员技巧。同样,这些是C语言的各个方面,不存在于机器级别,因此编译器不会为此烦恼。

创建一个vtable实例:

static const struct tiger_vtable_type tiger_vtable = {
  .setAge = animal_set_age,
  .sayHello = tiger_say_hello,
  .doTigerishThing = tiger_do_tigerish_thing,
};

并实现构造函数:

void
tiger_ctor(void * p, int age, int stripes)
{
  struct tiger * this = (struct tiger *) p;
  animal_ctor(this, age);
  this->base.vptr = &tiger_vtable;
  this->stripes = stripes;
}

老虎构造函数做的第一件事就是调用动物构造函数。还记得动物构造函数如何将vptr设置为&animal_vtable吗?这就是为什么从基类构造函数调用virtual成员函数让人惊讶的原因。只有在基类构造函数运行之后,我们才会将vptr重新分配给派生类型,然后进行自己的初始化。

operator new只是样板。

void *
tiger_new(int age, int stripes)
{
  void * p = malloc(sizeof(struct tiger));
  if (p != NULL)
    tiger_ctor(p, age, stripes);
  return p;
}

我们已经完成了。但是我们如何调用虚拟成员函数?为此,我将定义一个辅助宏。

#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...)                     \
  (*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS, __VA_ARGS__ )

现在,这很难看。它的作用是获取静态类型STYPEthis指针THIS以及成员函数FUNC的名称以及传递给函数的任何其他参数。

然后,它从静态类型构造vtable的类型名称。 (##是预处理程序的令牌粘贴运算符。例如,如果STYPEanimal,则STYPE ## _vtable_type将展开为animal_vtable_type。)

接下来,THIS指针被转换为指向刚刚派生的vtable类型的指针的指针。这是有效的,因为我们已确保将vptr作为第一个成员放在每个对象中,因此它具有相同的地址。这很重要。

完成此操作后,我们可以取消引用指针(获取实际的vptr),然后请求其FUNC成员并最终调用它。 (__VA_ARGS__扩展为其他可变参数宏参数。)请注意,我们还将THIS指针作为第0个参数传递给成员函数。

现在,实际情况是我必须为不带参数的函数再次定义一个几乎相同的宏,因为预处理器不允许可变参数宏参数包为空。它应该是。

#define INVOKE_VIRTUAL(STYPE, THIS, FUNC)                               \
  (*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS )

它有效:

#include <stdio.h>
#include <stdlib.h>

/* Insert all the code from above here... */

int
main()
{
  struct tiger * tp = tiger_new(7, 42);
  struct animal * ap = (struct animal *) tp;
  INVOKE_VIRTUAL(tiger, tp, sayHello);
  INVOKE_VIRTUAL(tiger, tp, doTigerishThing);
  INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
  INVOKE_VIRTUAL(animal, ap, sayHello);
  return 0;
}

您可能想知道

中会发生什么
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);

呼叫。我们正在做的是在通过setAge指针引用的Animal对象上调用Tiger的未被覆盖的struct tiger成员。该指针首先被隐式转换为void指针,因此作为this指针传递给animal_set_age。然后该函数将其强制转换为struct animal指针。它是否正确?这是因为我们谨慎地将struct animal作为struct tiger中的第一个成员,因此struct tiger对象的地址与struct animal的地址相同子对象。我们使用vptr玩的是相同的技巧(只有一个级别)。

答案 1 :(得分:2)

它可以帮助你自己实现类似的东西。

struct Bob;
struct Bob_vtable {
  void(*print)(Bob const*self) = 0;
  Bob_vtable(void(*p)(Bob const*)):print(p){}
};
template<class T>
Bob_vtable const* make_bob_vtable(void(*print)(Bob const*)) {
  static Bob_vtable const table(+print);
  return &table;
}
struct Bob {
  Bob_vtable const* vtable;
  void print() const {
    vtable->print(this);
  }
  Bob():vtable( make_bob_vtable<Bob>([](Bob const*self){
    std::cout << "Bob\n";
  })) {}
protected:
  Bob(Bob_vtable const* t):vtable(t){}
};
struct Alice:Bob {
  int x = 0;
  Alice():Bob( make_bob_vtable<Alice>([](Bob const*self){
    std::cout << "Alice " << static_cast<Alice const*>(self)->x << '\n';
  })) {}
};

live example

这里我们在Bob中存储了一个明确的vtable。它指向一个功能表。非虚拟成员函数print使用它来动态调度到正确的方法。

Bob和派生类Alice的构造函数将vtable设置为不同的值(在本例中创建为静态局部),表中包含不同的值。

使用哪个指针被绑定到Bob::print的含义定义中 - 它知道表中的偏移量。

如果我们在Alice中添加另一个虚函数,它只是意味着vtable指针实际上指向struct Alice_vtable:Bob_vtable。静态/重新解释转换将使我们成为“真正的”表,并且我们可以轻松访问额外的函数指针。

当我们谈论虚拟继承以及虚函数时,事情变得更加奇怪。我没有资格描述它是如何运作的。