虚函数可以进行内联吗?

9
如果我像这样定义一个类:

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

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


如果你仔细想想,内联虚函数并没有太多意义。我唯一能想到的情况是在编译时知道类型,但即使如此,我也不确定编译器是否会进行优化。 - Borgleader
https://dev59.com/fHRB5IYBdhLWcg3wEDul?rq=1 - Mat
@Borgleader:当能够执行时,它们会执行。然而,由于C++语言中有关多态对象构造和销毁的复杂规则,因此没有编译器真正擅长这个。此外,由于通常情况下没有JITing,可以执行的情况非常有限。 - Matthieu M.
2个回答

14
无论编译器是否选择内联定义为内联的函数,完全取决于编译器。一般来说,只有在编译器能够证明静态类型与动态类型匹配,或者能够安全地确定动态类型时,才能内联virtual函数。例如,当您使用类型A的值时,编译器知道动态类型不可能不同,并且可以内联该函数。当使用指针或引用时,编译器通常无法证明静态类型相同,因此virtual函数通常需要遵循常规的虚拟调度。然而,即使使用指针,编译器可能从上下文中获得足够的信息来知道确切的动态类型。例如,MatthieuM.给出了以下示例:
A* a = new B;
a->func();

在这种情况下,编译器可以确定变量a指向一个B对象,因此可以调用正确版本的func()而无需进行动态分派。没有动态分派的需要,func()可以被内联。当然,编译器是否执行相应的分析取决于其具体实现。
正如hvd正确指出的,可以通过调用带有完全限定名的虚函数来规避虚分派,例如a->A::func(),在这种情况下,虚函数也可以被内联。虚函数通常不会被内联的主要原因是需要进行虚分派。但是,通过完全限定名,要调用的函数是已知的。

3
对虚函数的非虚函数调用(a->A::func())是另一个通常可进行内联的明显示例。 - user743382
3
当编译器可以证明静态类型与动态类型匹配时,情况比这更加复杂。考虑以下代码:Base* b = new Derived{}; b->func();,如果编译器足够聪明地意识到 b 的动态类型必然是 Derived,则该调用可以被内联化。Clang 就是这样一个聪明的编译器。 - Matthieu M.
@Matthieu M.:所以Clang可以在编译时知道RTTI,太神奇了。 - Ghostblade
1
@Ghostblade:更多的是追踪值的来源并查看它是否可以解析为具体类型的游戏 :) 不过,似乎很遗憾要做这种追踪,因为 LLVM 也会这样做 :/ - Matthieu M.
1
@Ghostblade:不,我的意思是在某些条件下,正确的析构函数也可以在编译时确定。从这个意义上说,析构函数并不特殊。当正确的析构函数在编译时确定时,它可以是内联的。另一部分只是描述了虚拟调度对析构函数的特殊性,但这并不影响析构函数是否可以内联。 - Dietmar Kühl
显示剩余4条评论

5
是的,有多种方式。您可以在我大约两年前发送到Clang邮件列表中的这封电子邮件中看到一些关于虚函数消除的例子。
像所有优化一样,这取决于编译器是否具有消除备选项的能力:如果它可以证明虚调用始终在Derived::func中解决,则可以直接调用它。
有各种情况,让我们先从语义证据开始:
- SomeDerived& d是final时,允许消除所有方法调用 - SomeDerived& d,d.foo()其中foo是final时,也允许消除此特定调用
然后,有些情况下您知道对象的动态类型:
- SomeDerived d; => d的动态类型必须是SomeDerived - SomeDerived d; Base& b; => b的动态类型必须是SomeDerived
这4种虚拟化情况通常由编译器前端解决,因为它们需要对语言语义有基本的了解。我可以证明,在Clang中实现了这4种情况,并且我认为它们也在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);
}

尽管这里很明显调用了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行(目前)并且有详细的注释。


对代码的参考点赞。 - Surt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接