免责声明:编译器优化
这个答案是关于编译器优化技术的。您的编译器可能支持也可能不支持这些。即使它支持您尝试利用的技术,它们也可能无法在您选择的优化级别上使用。
您的里程可能会有所不同。
最多派生类
编译器确实可以去虚拟化调用如果它知道这确实是最派生的类。讨论如何实现这一点here。一个例子:
struct Base { virtual void call_me_virtual() = nullptr; };
struct Derived final : Base { void call_me_virtual() override { } };
void dosomething(Derived* d) {
d->call_me_virtual();
}
有趣的是,如果不这样做,您总是可以在另一个翻译单元中让其他人从您的类中派生出,因此编译器不会知道您当前的“更多派生”类翻译单元。
另一种确保类必须是最多派生的方法是将其放入匿名命名空间:
struct Base { virtual void call_me_virtual() = nullptr; };
namespace {
struct Derived : Base { void call_me_virtual() override { } };
void dosomething(Derived* d) {
d->call_me_virtual();
}
}
这是可行的,因为Derived 在当前翻译单元之外是未知的,这意味着任何更多派生类必须在当前翻译单元内。然而,这意味着 dosomething 不能(正确地)从当前编译单元外部调用,这也是我给它内部链接的原因。
这个规则的一个例外是编译器能够证明对象来自他们的视图,例如。如果Derived 是当前翻译单元中派生最多的类型,并且对象必须始终来自当前翻译单元,则它不能来自任何其他派生类型。通过利用整个程序优化可以扩大这种分析的范围。
已知类型
更常见的情况是编译器确切地知道对象是什么类型,例如在这两种情况下:
Derived d;
d.call_me_virtual();
Base* b = new Derived;
b->call_me_virtual();
编译器可能能够推断出d.call_me_virtual 和b->call_me_virtual 都将始终解析为Derived::call_me_virtual,从而在该实例中对调用进行去虚拟化(甚至内联)。
在第一种情况下,这需要知道Derived 类型的对象与Derived& 类型非常相似的对象只能是Derived 对象,而不是从它派生的对象。
第二种情况需要静态类型分析,这是由现代优化编译器完成的。通过看到b总是使用指向Derived 对象的指针进行初始化,编译器能够证明b->call_me_virtual 将调用哪个函数。
最终确定的成员函数
当你完成单个成员函数时会发生类似的情况:
struct Base { virtual void call_me_virtual() = nullptr; };
struct Derived : Base { void call_me_virtual() override final { } };
void dosomething(Derived* d) {
d->call_me_virtual();
}
虽然d 可能指向派生自Derived 的内容,但call_me_virtual 方法可能不再更改,因此编译器知道Derived::call_me_virtual 将始终被调用,因此可以取消虚拟化此调用。