为什么虚拟的成员函数会影响实际上不需要虚拟调度的 TU 的编译代码?

Why a member function being virtual affects the compiled code of a TU where no virtual dispatch is actually required?

提问人:Enlico 提问时间:9/18/2023 最后编辑:Enlico 更新时间:9/18/2023 访问量:83

问:

在这样的 TU 中

#include "Foo.hpp"
int main() {
    // stuff
    Foo* foo{new Foo{}};
    foo->foo();
    // stuff
}

其中包含Foo.hpp

#pragma once
struct Foo {
    virtual void foo(); // implmented somewhere
};

除了可能发生之外,没有其他任何事情,对吧? 是 也不是 它 也不是 类 是 ,所以是的,在另一个 TU 中可能有派生类的对象,依此类推,但是......就这个 TU 而言,我认为很明显,调用 .我不明白怎么会是这样。Foo::foofoovirtualfinaloverridefoofoo->foo()Foo::foo()

那为什么生成的程序集是这样的呢?

main:                                   # @main
        push    rax
        mov     edi, 8
        call    operator new(unsigned long)@PLT
        mov     rcx, qword ptr [rip + vtable for Foo@GOTPCREL]
        add     rcx, 16
        mov     qword ptr [rax], rcx
        mov     rdi, rax
        call    Foo::foo()@PLT
        xor     eax, eax
        pop     rcx
        ret

我并没有真正详细地理解它,但我清楚地阅读了.为什么它甚至在那里?vtable

我本来希望上面的 TU 的组装与我删除关键字时得到的组装相同:virtual

main:                                   # @main
        push    rax
        mov     edi, 1
        call    operator new(unsigned long)@PLT
        mov     rdi, rax
        call    Foo::foo()@PLT
        xor     eax, eax
        pop     rcx
        ret

(下面是 CE 上的示例

另一个答案中,我读到

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

在这种情况下,编译器可以确定指向对象,从而调用正确的版本,而无需动态调度。[...]当然,编译器是否进行相应的分析取决于其各自的实现。aBfunc()

答案是否只是 Clang 没有进行分析,从而推断出不需要运行时调度?

还是我错过了什么?也许我只是完全误解了大会?

C++ 继承 函数 动态调度 虚拟表

评论

0赞 Enlico 9/18/2023
@273K,我已经阐明了该类是在标头中定义的。这只是说,我理解虚拟调用可以在链接到此转换单元的其他翻译单元中进行(也许该部分确实通过指向基类的指针使用派生对象)。// stuff
0赞 273K 9/18/2023
foo()没有身体,真正的绑定留到链接状态。添加正文:godbolt.org/z/eosx6nG3M
1赞 273K 9/18/2023
Clang 不进行分析事实并非如此。如您所见,它直接调用。不使用取消引用的 vtable。这将是另一个问题,为什么 clang 不用户取消引用 vtable 结果。Foo::foo()
2赞 Evg 9/18/2023
引用 是 的 vtable 指针初始化。vtable*foo
1赞 Evg 9/18/2023
即使直接调用,您仍然需要为调用该虚拟函数的对象设置一个 vtable 指针。如果你在里面做怎么办?foo()foo()typeid(*this)

答:

2赞 n. m. could be an AI 9/18/2023 #1

这条线

mov     rcx, qword ptr [rip + vtable for Foo@GOTPCREL]

初始化 vtable 指针。

vtable 机制不用于调用 ,但是每个具有 vtable 的对象都需要初始化其 vtable 指针。如果不初始化对象的 vtable 指针,可能会中断(例如,它可以尝试和)。Foo::fooFooFoo::foo()dynamic_castthis

如果你给出一个可内联的主体,编译器可能会(也可能不会)优化整个对象,这取决于主体中的具体内容。但是,编译器不太可能允许存在具有未初始化的 vtable 指针的对象。实在是太麻烦了,不值得费力去处理这个边缘案件。Foo::fooFoo

5赞 user17732522 9/18/2023 #2

您显示的程序集中的 的调用已解析为对 的直接调用。fooFoo::foo

对 vtable 的引用是必需的,因为指向它的指针需要存储在对象中。的实现可能依赖于它,例如通过使用 、 ,甚至只是递归调用 on 。FooFoo::foodynamic_casttypeidfoothis

评论

0赞 StoryTeller - Unslander Monica 9/18/2023
事实上。不出所料,当内联定义为空时,会优化为 .foomainreturn 0;