c++ 虚函数和虚表如何工作?

thigvfpy  于 2023-04-01  发布在  其他
关注(0)|答案(1)|浏览(128)

虽然C++标准将虚拟分派的实现留给了编译器,但目前只有3个主要的编译器(gcc、clang和msvc)。
当你通过一个指向抽象基的指针调用一个抽象基上的方法时,他们是如何实现虚分派的?在构造过程中如何设置虚表?
一个简单的“好像”的例子会很有用。

ar7v8xwq

ar7v8xwq1#

每个主要的C++实现都使用vtable。这是一个指向方法指针数组(或结构体)的指针。对于给定类型有一个vtable(不包括涉及动态加载的一些角落情况)。
如果你这样写:

class Iswitch {
public:
  virtual void turn_on() = 0;
  virtual void turn_off() = 0;
  virtual ~Iswitch() = default;
};

编译器创建了类似于下面的内容:

struct switch_vtable {
  void(*turn_on)(void*) = 0;
  void(*turn_off)(void*) = 0;
  void(*dtor)(void*) = 0;
};

在C++编译器生成的汇编级,一个封装函数指针数组和一个封装函数指针结构是相同的。

struct Iswitch {
  switch_vtable const* vtable = 0;
  Iswitch() {};
  void dynamic_destroy() { vtable->dtor(this); }
  void turn_on() { vtable->turn_on(this); }
  void turn_off() { vtable->turn_off(this); }
};

当你从Iswitch继承时:

class MySwitch:public Iswitch {
  std::string message;
public:
  void turn_on() final { std::cout << message << " turns on\n"; }
  void turn_off() final { std::cout << message << " turns off\n"; }
};

编译器生成类似于以下的输出:

struct MySwitch:public Iswitch {
  static MySwitch* from_pvoid(void* self) {
    return static_cast<MySwitch*>(static_cast<Iswitch*>(self));
  }
  static switch_vtable make_MySwitch_vtable() {
    return {
      [](void* self){ from_pvoid(self)->turn_on_impl(); },
      [](void* self){ from_pvoid(self)->turn_off_impl(); },
      [](void* self){ from_pvoid(self)->~MySwitch(); }
    };
  }
  static switch_vtable const* get_MySwitch_vtable() {
    static const auto vtable = make_MySwitch_vtable();
    return &vtable;
  }

  std::string message;
  void turn_on_impl() { std::cout << message << " turns on\n"; }
  void turn_off_impl() { std::cout << message << " turns off\n"; }
  MySwitch() { vtable = get_MySwitch_vtable(); }
};

当创建派生对象时,它将基对象中的vtable指针设置为指向其vtable,作为其构造函数的一部分。
然后,当您通过虚拟分派(通常只是调用它)访问方法时,代码在vtable中查找方法指针并调用它。
如果你有

void off_and_on( Iswitch* pswitch ) {
  pswitch->turn_off();
  pswitch->turn_on();
}

无论pswitch指向Iswitch的哪个派生类,(在此函数中)运行的代码都是相同的;至少直到它到达turn_onturn_off的主体。

MySwitch bob;
on_and_off(&bob);

这在IswitchMySwitchclass“compiler does it for me”版本中所做的事情与在手册struct版本中所做的事情基本相同。
dynamic_destroy是我添加的一个特殊的助手。当你通过一个虚拟析构函数指针删除一个C++对象时,它会在vtable中查找析构函数并使用它来清理对象。

Iswitch* pswitch = new MySwitch;
delete pswitch;

在手动struct的情况下,上述内容变为:

// Iswitch* pswitch = new MySwitch; translates to:
Iswitch* pswitch = ::new( malloc( sizeof(MySwitch) ) ) MySwitch{};
// (with an extra try-catch to clean up the malloc memory if the ctor throws)
// delete pswitch; translates to:
pswitch->dynamic_destroy();
free(pswitch);
// (usually dtors are nothrow, so no try-catch needed here)

在这里,您可以看到the two versions并行编译。
在真实的生成的代码中有一些差异。
1.我创建了helper函数,而真实的生成的代码内联了它们。

  1. vtable函数的调用约定通常与普通函数不同。
  2. gcc在vtable中生成2个析构函数helper;一个破坏,另一个破坏和释放记忆。我只是破坏。
    1.在gcc生成的vtable中有RTTI(运行时类型信息)。这是动态强制转换之类的东西所需要的。
    此外,如果使用virtual继承,事情会变得更有趣,因为现在vtable中有指向vtable的指针,而不仅仅是方法。
    如果我们扩展Iswitch接口,我们只会得到一个更大的array/vtable结构体,其中扩展的vtable的第一部分与基类匹配。
class Iswitch_extended:public Iswitch {
  virtual bool is_on() const = 0;
};

对应于:

struct Iswitch_extended_vtable:Iswitch_vtable {
  bool(*is_on)(void const*)=0;
};

struct Iswitch_extended:public Iswitch {
  Iswitch_extended_vtable const* get_vtable() const { return static_cast<Iswitch_extended_vtable const*>(vtable); }

  bool is_on() const { return get_vtable()->is_on(this); }
};

并且实现它的类必须指向一个完整的Iswitch_extended表和vtable
所有这些技术在C++被指定之前就存在于C代码库中。这个想法是,编写这个样板文件很烦人,让编译器为您编写它是很好的。
缺点是有不止一种方法来完成所有这些。虽然每个主要的编译器都使用上述技术,但MFC使用基于开关的分派机制来实现其多态性。
上面的vtable方法的问题是每个具体类都需要一个O在MFC的情况下,需要重写的方法的数量可能很大(几乎所有的windows消息!)。所以消息ID不是一个巨大的函数表,而是传递给调度函数,它是菊花链的。如果一个子类想拦截这个对象上的那个消息,它停止链并返回函数指针(或直接在函数指针上执行消息)。
但是,由于语言中内置了一个固定的动态分派代码生成器,替代方法的成本要高得多,并且被忽视,即使它们对于特定的用例更好。
就我个人而言,我很期待反射,有了反射,我们就能像编译器一样高效地生成动态调度代码,但目标是不同的OO模型选择。

相关问题