当从派生的立即对象间接调用非重写基方法中的虚拟方法时,是否执行vtable查找
Is a vtable lookup performed when indirectly calling a virtual method in a non-overridden base method from a derived immediate object?
这个问题是这个问题的一个稍微复杂一些的版本,已经得到了很好的回答。在答案中使用的语义中,我指的是虚拟调用的实现级别(即通常的vtable查找(。
基类使用在某些派生类中重写的虚拟成员来实现成员(通常是非虚拟的(。当使用派生的立即对象(不涉及指针或引用(来调用此成员时,是否涉及vtable查找?
以下是我想到的最简单的场景:
class A
{
public:
void generic_method()
{
// Do some stuff
specialized_method();
}
virtual void specialized_method(); // Details are useless here.
};
class B : public A
{
public:
void specialized_method() override;
};
int main()
{
// I don't want neither need type resolution at runtime.
// Using the immediate object
B b;
// Is there a vtable lookup for the indirect `specialized_method` call here?
b.generic_method();
}
涉及的每个类型都可以在编译时解析。因此,在这种情况下,我期待着直接调用,但我是否以某种方式阻止了这样的优化?
更多上下文
我不喜欢在我的用例中依赖编译器优化。以下是我正在努力实现的目标。任何建议都是受欢迎的。
generic_method
是一种算法- 这依赖于某个容器的初始化。在这里,我希望用户可以完全自由地设置每个值。但是我想保证容器的结构是有效的
- 因此,我将初始化母类中的结构(这不是微不足道的(,并让用户重写返回每个值(代码中的
specific_method
(的函数来填充容器。它将用于每个容器入口 - 这个容器通常非常大,而且特定的方法非常快,所以虚拟调用开销是相关的
我的想法:
- 不要依赖继承,而是使用lambda表达式作为参数。但是
this
不会被捕获,我不想更改访问说明符 - 我可以把初始化这个容器的责任交给每个子类的构造函数。但我不能强制执行,也不能在代码中真正明确:我必须依靠文档来激励程序员用户以我想要的安全方式初始化容器
- 使用纯虚拟方法作为专用方法会在编译时强制类型的解析并防止vtable查找吗
- 使用奇怪的重复模板模式(CRTP(来实现静态多态性
在A::generic_method
中:由于specialized_method();
实际上是this->specialized_method();
,因此将执行动态绑定(例如vtable查找(。
B::generic_method
是从A继承的,所以b.generic_method()
是到A::generic_method
的静态绑定(在这个调用中有我上面讨论过的specialized_method
的动态绑定(
现代编译器可以优化并完全跳过vtable查找,如果它们可以";参见";对象的真实类型(优化称为去机会化(。gcc在我所有的测试中都表现出了失误,而(令我惊讶的是(clang在大多数情况下都不能。
用CCD_ 11对gcc 10.2和clang 11.0.0进行了各项试验。
情况:没有虚拟析构函数
导螺杆连杆
情况1:可以跳过vtable查找
auto t1()
{
B b{};
b.generic_method();
}
auto t2(B b)
{
b.generic_method();
}
对于t1
,gcc和clang都跳过vtable查找,直接调用B::specialized_method()
。对于t2
,只有gcc执行优化:
gcc输出:
t1():
sub rsp, 24
mov QWORD PTR [rsp+8], OFFSET FLAT:_ZTV1B+16
lea rdi, [rsp+8]
call B::specialized_method()
add rsp, 24
ret
t2(B):
jmp B::specialized_method()
Clang输出
t1(): # @t1()
push rax
mov qword ptr [rsp], offset vtable for B+16
mov rdi, rsp
call B::specialized_method()
pop rax
ret
t2(B): # @t2(B)
mov rax, qword ptr [rdi]
jmp qword ptr [rax] # TAILCALL
情况2:绑定必须是动态的,不能被破坏:
auto t3(B& b)
{
b.generic_method();
}
auto t4(B* b)
{
b->generic_method();
}
t3(B&): # @t3(B&)
mov rax, qword ptr [rdi]
jmp qword ptr [rax] # TAILCALL
t4(B*): # @t4(B*)
mov rax, qword ptr [rdi]
jmp qword ptr [rax] # TAILCALL
情境:虚拟析构函数
导螺杆连杆
情况1:可以跳过vtable查找
auto t1()
{
B b{};
b.generic_method();
}
auto t2(B b)
{
b.generic_method();
}
auto t5()
{
std::unique_ptr<B> b = std::make_unique<B>();
b->generic_method();
}
auto t6()
{
std::unique_ptr<A> b = std::make_unique<B>();
b->generic_method();
}
auto t7()
{
B* b = new B{};
b->generic_method();
delete b;
}
auto t8()
{
A* b = new B{};
b->generic_method();
delete b;
}
Gcc对所有的例子都执行去机会化,而clang对任何例子都不执行:
Gcc输出:
t1():
sub rsp, 24
mov QWORD PTR [rsp+8], OFFSET FLAT:_ZTV1B+16
lea rdi, [rsp+8]
call B::specialized_method()
add rsp, 24
ret
t2(B):
jmp B::specialized_method()
t5():
push r12
mov edi, 8
push rbp
sub rsp, 8
call operator new(unsigned long)
mov QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov rdi, rax
mov rbp, rax
call B::specialized_method()
mov rax, QWORD PTR [rbp+0]
mov rdi, rbp
mov rax, QWORD PTR [rax+16]
add rsp, 8
pop rbp
pop r12
jmp rax
mov r12, rax
jmp .L8
t5() [clone .cold]:
.L8:
mov rax, QWORD PTR [rbp+0]
mov rdi, rbp
call [QWORD PTR [rax+16]]
mov rdi, r12
call _Unwind_Resume
t6():
push r12
mov edi, 8
push rbp
sub rsp, 8
call operator new(unsigned long)
mov QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov rdi, rax
mov rbp, rax
call B::specialized_method()
mov rax, QWORD PTR [rbp+0]
mov rdi, rbp
mov rax, QWORD PTR [rax+16]
add rsp, 8
pop rbp
pop r12
jmp rax
mov r12, rax
jmp .L12
t6() [clone .cold]:
.L12:
mov rax, QWORD PTR [rbp+0]
mov rdi, rbp
call [QWORD PTR [rax+16]]
mov rdi, r12
call _Unwind_Resume
t7():
push rbp
mov edi, 8
call operator new(unsigned long)
mov QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov rbp, rax
mov rdi, rax
call B::specialized_method()
mov rax, QWORD PTR [rbp+0]
mov rdi, rbp
pop rbp
mov rax, QWORD PTR [rax+16]
jmp rax
t8():
push rbp
mov edi, 8
call operator new(unsigned long)
mov QWORD PTR [rax], OFFSET FLAT:_ZTV1B+16
mov rbp, rax
mov rdi, rax
call B::specialized_method()
mov rax, QWORD PTR [rbp+0]
mov rdi, rbp
pop rbp
mov rax, QWORD PTR [rax+16]
jmp rax
叮当声输出:
t1(): # @t1()
push rax
mov qword ptr [rsp], offset vtable for B+16
mov rdi, rsp
call qword ptr [rip + vtable for B+16]
pop rax
ret
t2(B): # @t2(B)
mov rax, qword ptr [rdi]
jmp qword ptr [rax] # TAILCALL
t5(): # @t5()
push r14
push rbx
push rax
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov qword ptr [rax], offset vtable for B+16
mov rdi, rax
call qword ptr [rip + vtable for B+16]
mov rax, qword ptr [rbx]
mov rdi, rbx
add rsp, 8
pop rbx
pop r14
jmp qword ptr [rax + 16] # TAILCALL
mov r14, rax
mov rax, qword ptr [rbx]
mov rdi, rbx
call qword ptr [rax + 16]
mov rdi, r14
call _Unwind_Resume
t6(): # @t6()
push r14
push rbx
push rax
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov qword ptr [rax], offset vtable for B+16
mov rdi, rax
call qword ptr [rip + vtable for B+16]
mov rax, qword ptr [rbx]
mov rdi, rbx
add rsp, 8
pop rbx
pop r14
jmp qword ptr [rax + 16] # TAILCALL
mov r14, rax
mov rax, qword ptr [rbx]
mov rdi, rbx
call qword ptr [rax + 16]
mov rdi, r14
call _Unwind_Resume
t7(): # @t7()
push rbx
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov qword ptr [rax], offset vtable for B+16
mov rdi, rax
call qword ptr [rip + vtable for B+16]
mov rax, qword ptr [rbx]
mov rdi, rbx
pop rbx
jmp qword ptr [rax + 16] # TAILCALL
t8(): # @t8()
push rbx
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov qword ptr [rax], offset vtable for B+16
mov rdi, rax
call qword ptr [rip + vtable for B+16]
mov rax, qword ptr [rbx]
mov rdi, rbx
pop rbx
jmp qword ptr [rax + 16] # TAILCALL
情况2:绑定必须是动态的,不能被破坏:
auto t3(B& b)
{
b.generic_method();
}
auto t4(B* b)
{
b->generic_method();
}
t3(B&): # @t3(B&)
mov rax, qword ptr [rdi]
jmp qword ptr [rax] # TAILCALL
t4(B*): # @t4(B*)
mov rax, qword ptr [rdi]
jmp qword ptr [rax] # TAILCALL
generic_method()
函数有一个隐式A* const this
参数,因此您错误地认为没有涉及指针或引用。函数的主体不知道是谁在调用它,所以它需要进行vtable查找,以确定要调用哪个specialized_method()
函数。
想象一下A
是在某个库中定义的,我编写了一个从A
派生的新的C
类。这个库已经编译好了,正被链接到我的程序中,它怎么知道它需要调用C::specialized_method()
?
理论上,编译器可能有足够的知识来知道generic_method()
只在B
对象上被调用,或者它可以为每次调用内联generic_method()
,但你不应该依赖于此。除了最简单的例子之外,它也不太可能做到这一点。
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 在模板基类中为继承类中的可选重写生成虚拟方法
- 跨 DLL 边界访问虚拟方法是否安全/可能?
- 是否可以使用基类非虚拟方法中的派生类虚拟方法?
- 如何编写 operator= 用于使用虚拟方法与非平凡成员的匿名联合
- 让编译器告诉什么确切的纯虚拟方法使结构抽象?
- 使用模板而不是虚拟方法的管道模式
- 派生类调用父类的方法,该方法调用重写的虚拟方法调用错误的方法
- 从纯虚拟类 (A) 派生的指针无法访问来自纯类 (B) 的重载方法
- 为什么调用没有正文的纯虚拟方法不会导致链接器错误?
- 出于什么目的,非虚拟方法将与C++一起使用?
- 为什么使用存储在虚拟方法表中的地址调用虚拟函数的函数会返回垃圾?
- 如何重写继承的嵌套类中存在的虚拟方法
- 私有虚拟方法有什么用?
- 派生类中纯虚拟基方法的专业化
- 基类可以声明虚拟方法但不定义它吗?仍然在派生类中定义
- googletest:测试基类具有纯虚拟方法的派生类时的核心转储
- 确保模拟的 GTest 方法覆盖虚拟方法
- 使用回调函数从构造函数调用虚拟/派生方法的替代方法?
- 如何调用孩子的方法:虚拟关键字不起作用