考虑以下代码:
struct A {
virtual A& operator+=(const A& other) noexcept = 0;
};
void foo_inner(int *p) noexcept { *p += *p; }
void foo_virtual_inner(A *p) noexcept { *p += *p; }
void foo(int *p) noexcept
{
return foo_inner(p);
}
struct Aint : public A {
int i;
A& operator+=(const A& other) noexcept override final
{
// No devirtualization of foo_virtual with:
i += dynamic_cast<const Aint&>(other).i;
// ... nor with:
// i += reinterpret_cast<const Aint&>(other).i;
return *this;
}
};
void foo_virtual(Aint *p) noexcept
{
return foo_virtual_inner(p);
}
据我所知,foo()
和foo_virtual()
都应编译为相同的目标代码。从operator+=
调用foo_virtual_inner()
时,编译器具有将虚拟化对foo_virtual
中的-O3
的调用所需的全部信息。但是-neither GCC 8.3, nor MSVC 19.10, nor clang 8 do this。自然,我使用了最大优化标志(/Ox
或foo(int*): # @foo(int*)
shl dword ptr [rdi]
ret
foo_virtual(Aint*): # @foo_virtual(Aint*)
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax]
mov rsi, rdi
jmp rax # TAILCALL
)。
为什么?这是一个错误,还是我缺少什么?
clang 8输出:
foo(int*):
sal DWORD PTR [rdi]
ret
foo_virtual(Aint*):
mov rax, QWORD PTR [rdi]
mov rax, QWORD PTR [rax]
cmp rax, OFFSET FLAT:Aint::operator+=(A const&)
jne .L19
push rbx
xor ecx, ecx
mov edx, OFFSET FLAT:typeinfo for Aint
mov esi, OFFSET FLAT:typeinfo for A
mov rbx, rdi
call __dynamic_cast
test rax, rax
je .L20
mov eax, DWORD PTR [rax+8]
add DWORD PTR [rbx+8], eax
pop rbx
ret
.L19:
mov rsi, rdi
jmp rax
foo_virtual(Aint*) [clone .cold.1]:
.L20:
call __cxa_bad_cast
GCC 8.3输出:
p$ = 8
void foo(int * __ptr64) PROC ; foo
mov eax, DWORD PTR [rcx]
add eax, eax
mov DWORD PTR [rcx], eax
ret 0
void foo(int * __ptr64) ENDP ; foo
p$ = 8
void foo_virtual(Aint * __ptr64) PROC ; foo_virtual
mov rax, QWORD PTR [rcx]
mov rdx, rcx
rex_jmp QWORD PTR [rax]
void foo_virtual(Aint * __ptr64) ENDP
MSVC 19.10输出:
[ [-1, 0 , 1], [-1, 0 , 1], [-1, 0 , 1] ]
PS-在GCC下编译的代码中对所有typeinfo业务的解释是什么?
答案 0 :(得分:1)
GCC猜测Aint * p指向Aint * p的实例(但不认为一定会发生这种情况),因此它推测性地取消了对operator + =的调用,并且typeinfo检查是它的内联副本。 -fno-devirtualize-以推测方式导致与Clang和MSVC产生的代码相同。
_Z11foo_virtualP4Aint:
.LFB4:
.cfi_startproc
movq (%rdi), %rax
movq %rdi, %rsi
movq (%rax), %rax
jmp *%rax
答案 1 :(得分:0)
答案 2 :(得分:0)
编译器无法假定Aint *实际上指向Aint对象,直到它看到否则会具有未定义语义的某些操作,例如引用其非静态成员之一。否则,可能是其他某种指针类型的reinterpret_cast结果,等待重新解释回该类型。
在我看来,将标准转换为A *应该是这样的操作,但是AFAICT标准目前并未这么说。要达到这种效果,需要考虑将其转换为正在构造的对象的非虚拟基础,这是有意允许的。