为何虚成员函数会影响未触发虚分派的编译单元生成代码?
问题场景代码
翻译单元(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 };
汇编对比
保留virtual关键字时的汇编
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
移除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
疑问与解答
从代码逻辑看,foo->foo()必然调用Foo::foo(),但保留virtual时的汇编出现了vtable相关指令,这并不是Clang没做优化,而是对汇编的理解有偏差:
动态分派已经被优化:
注意看汇编里的调用指令是call Foo::foo()@PLT——这是直接调用具体函数的方式。如果没做动态分派消除优化,这里应该是通过虚表间接调用,比如call qword ptr [rax](先取对象的虚表指针,再从虚表中取函数地址调用)。所以Clang已经识别出当前场景不需要动态分派,直接优化成了静态调用。vtable相关指令是对象初始化的要求:
带virtual的类属于多态类,C++标准要求这类对象的内存布局必须包含虚表指针(vptr),指向对应的虚表(vtable)。哪怕当前TU里不需要用这个虚表做动态分派,new Foo{}构造出的对象也必须符合多态类的内存规范——因为这个对象可能被其他TU的代码使用(比如把指针传递出去,其他代码可能通过它做动态分派)。
汇编里的mov rcx, qword ptr [rip + vtable for Foo@GOTPCREL]等指令,就是在给新创建的Foo对象初始化虚表指针,这是构造多态对象的必要步骤,和当前的函数调用优化无关。内存分配大小的差异原因:
保留virtual时分配8字节(mov edi,8),是因为64位系统下虚表指针占8字节;移除virtual后分配1字节(mov edi,1),是因为空结构体的大小在C++中是1字节(保证每个对象有唯一地址),这也侧面印证了多态对象需要额外存储虚表指针的事实。
内容的提问来源于stack exchange,提问作者Enlico

