9

如果我这样定义一个类:

class A{
public:
    A(){}
    virtual ~A(){}
    virtual void func(){}
};

这是否意味着虚拟析构函数和func被内联

4

2 回答 2

12

编译器是否选择内联定义为内联的函数完全取决于编译器。通常,virtual只有当编译器可以证明静态类型与动态类型匹配或者编译器可以安全地确定动态类型时,才能内联函数。例如,当您使用类型A的值时,编译器知道动态类型不能不同,它可以内联函数。当使用指针或引用时,编译器通常无法证明静态类型是相同的,virtual函数通常需要遵循通常的虚拟调度。但是,即使使用了指针,编译器也可能从上下文中获得足够的信息来了解确切的动态类型。例如,马蒂厄姆。给出了以下示例:

A* a = new B;
a->func();

在这种情况下,编译器可以确定a指向一个B对象,因此,调用正确版本的func()没有动态分派。不需要动态调度,func()然后可以内联。当然,编译器是否做相应的分析取决于其各自的实现。

正如 hvd 正确指出的那样,可以通过调用一个完全限定的虚函数来规避虚拟调度,例如,a->A::func()在这种情况下,也可以内联虚函数。虚拟函数通常不内联的主要原因是需要进行虚拟调度。然而,通过完全限定,要调用的函数是已知的。

于 2013-08-25T18:06:56.230 回答
4

是的,而且有多种方式。您可以在大约 2 年前发送到 Clang 邮件列表的这封电子邮件中看到一些去虚拟化的示例

像所有优化一样,这是等待编译器消除替代方案的能力:如果它可以证明虚拟调用总是被解析,Derived::func那么它可以直接调用它。

有多种情况,让我们首先从语义证据开始:

  • SomeDerived& dwhere SomeDerivedisfinal允许对所有方法调用进行去虚拟化
  • SomeDerived& d, d.foo()where fooisfinal还允许对这个特定的调用进行去虚拟化

然后,在某些情况下您知道对象的动态类型:

  • SomeDerived d;=> 的动态类型d是必然的SomeDerived
  • SomeDerived d; Base& b;=> 的动态类型b是必然的SomeDerived

这 4 种去虚拟化情况通常由编译器前端解决,因为它们需要有关语言语义的基础知识。我可以证明所有 4 个都是在 Clang 中实现的,我认为它们也在 gcc 中实现。

但是,有很多情况会发生这种情况:

struct Base { virtual void foo() = 0; };
struct Derived: Base { virtual void foo() { std::cout << "Hello, World!\n"; };

void opaque(Base& b);
void print(Base& b) { b.foo(); }

int main() {
    Derived d;

    opaque(d);

    print(d);
}

尽管这里的调用很明显foo被解析为Derived::foo,但 Clang/LLVM 不会对其进行优化。问题是:

  • Clang(前端)不执行内联,因此它不能替换print(d)d.foo()去虚拟化调用
  • LLVM(后端)不知道该语言的语义,因此即使在替换print(d)d.foo()它之后也假设 的虚拟指针d可能已被更改opaque(其定义是不透明的,顾名思义)

我一直关注 Clang 和 LLVM 邮件列表上的努力,因为两组开发人员都在推理信息丢失以及如何让 Clang 告诉 LLVM:“没关系”,但不幸的是,这个问题并不重要,还没有解决...因此,前端半途而废的去虚拟化尝试获取所有明显的案例,以及一些不那么明显的案例(尽管按照惯例,前端不是您实现它们的地方)。


作为参考,Clang 中的去虚拟化代码可以在CGExprCXX.cpp的一个名为canDevirtualizeMemberFunctionCalls. 它只有约 64 行长(现在)并且经过彻底的评论。

于 2013-08-25T18:44:49.147 回答