虽然C++标准将虚拟分派的实现留给编译器,但目前只有3个主要的编译器(gcc、clang和msvc)。
当您通过指向该抽象基类的指针调用抽象基类上的方法时,它们如何实现虚拟分派?在构造期间vtable是如何设置的?
一个简单的“好像”示例会很有用。
虽然C++标准将虚拟分派的实现留给编译器,但目前只有3个主要的编译器(gcc、clang和msvc)。
当您通过指向该抽象基类的指针调用抽象基类上的方法时,它们如何实现虚拟分派?在构造期间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;
};
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(); }
};
void off_and_on( Iswitch* pswitch ) {
pswitch->turn_off();
pswitch->turn_on();
}
这个函数中运行的代码是相同的,无论 pswitch
指向哪个继承自 Iswitch
的派生类;至少在进入 turn_on
或 turn_off
函数体之前。
MySwitch bob;
on_and_off(&bob);
这在Iswitch
和MySwitch
的“编译器自动完成”版本中的class
与手动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)
在这里,您可以看到两个版本并排编译的情况。
在实际生成的代码中,有一些差异。
此外,如果您使用virtual
进行继承,则会变得更加有趣,因为现在您的vtable中有指向vtable的指针,而不仅仅是方法。
如果我们扩展Iswitch
接口,我们只会得到一个更大的数组/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); }
};
实现它的类必须指向具有vtable的完整表。
在C++被指定之前,所有这些技术都存在于C代码库中。当Bjarn设计C++时,他考虑到了这些东西;这个想法是写这个样板很烦人,让编译器为你写是很好的。
缺点是有多种方法来完成所有这些。虽然每个主要的编译器都使用了上述技术,但MFC使用了基于开关的分派机制来实现其多态性。
上述vtable方法的问题在于,每个具体类都需要一个O(虚拟方法数)表。在MFC的情况下,需要可能重写的方法数量很大(几乎每个Windows消息!)。因此,消息ID被传递给分派函数,该函数被链接在一起。如果子类想要在此对象上拦截该消息,则停止链并返回函数指针(或直接在函数指针上执行消息)。
但是,由于语言中内置了动态分派代码的固定生成器,因此替代方法的成本要高得多,并且即使它们对于特定用例更好,也会被忽略。
就我而言,我期待反射技术的到来。有了反射技术,我们将能够像编译器一样高效地生成动态分派代码,但可以针对不同的面向对象模型进行优化。
struct
版本中,编译器会在去虚拟化时调用MySwitch::turn_on_impl()
而不是进行动态分派。 - Yakk - Adam Nevraumont