C++:使用指向派生类的指针的虚函数调用仍然具有vlookup

Coo*_*kie 7 c++ virtual inheritance templates

只是想知道,如果我有一个指向最派生类的指针,并在其上调用一个虚拟函数(大多数派生类定义),这是否仍会导致虚拟表中的查找?

毕竟,在编译时,编译器知道这个类是派生最多的,它知道它定义了虚函数,没有歧义,所以它应该只把它当作非虚函数?

或者我错过了什么?

我问的原因是我正在编写一个模板,我想从后面派生出来合并代码,不同的函数将在派生类中实现.

没有必要在模板中将这些函数定义为虚拟,但如果稍后忽略该虚拟调用,我正在考虑这样做,纯粹是为了向实现者可视化,以后需要仍然编写哪些函数.

gha*_*.st 5

免责声明:编译器优化

这个答案是关于编译器优化技术的.您的编译器可能支持也可能不支持这些.即使它支持您尝试利用的技术,它们也可能无法在您选择的优化级别上使用.

你的旅费可能会改变.

大多数派生类

如果编译器知道这确实是最派生的类,那么编译器确实可以对调用进行虚拟化.这里讨论如何实现这一目标.一个例子:

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();
}
Run Code Online (Sandbox Code Playgroud)

有趣的是,如果没有这样做,你可能总是让别人从你的类派生到另一个翻译单元,所以编译器不会知道你当前翻译单元中的那个"更多派生"类.

确保该类必须最多派生的另一种方法是将其放入匿名命名空间:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为Derived在当前翻译单元之外是未知的,这意味着任何更多的派生类必须在当前翻译单元内.但是,这意味着dosomething无法(正确地)从当前编译单元外部调用,这就是为什么我也给它内部链接.

此规则的一个豁免是能够证明对象来自其视图内的编译器,例如,如果Derived当前翻译单元中的派生类型最多且对象必须始终来自当前翻译单元,那么它不能是来自任何更多派生类型.通过利用整个程序优化,可以扩大该分析的范围.

已知类型

更常见的情况是编译器确切知道对象的类型,例如在以下两种情况下:

Derived d;
d.call_me_virtual();

Base* b = new Derived;
b->call_me_virtual();
Run Code Online (Sandbox Code Playgroud)

编译器可能能够推断出它 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();
}
Run Code Online (Sandbox Code Playgroud)

虽然d可能指向派生的东西Derived,但该call_me_virtual方法可能不再被更改,因此编译器知道Derived::call_me_virtual将始终被调用,因此可以对此调用进行虚拟化.