堆栈分配对象的 C++ 虚拟析构函数内联
C++ virtual destructor inlining for stack allocated objects
是否允许编译器在隐式销毁堆栈分配对象的情况下内联虚拟析构函数?
我理解 'delete' 操作必须通过虚函数 table 调用虚析构函数(例如它不能内联),因为它无法知道确切的 class指针指向。
但是当一个对象被分配到栈上时,编译器知道确切的class。所以我认为可以自由地内联隐式破坏,因为它可以看到 class 的实际析构函数。如果不允许编译器这样做,那为什么不呢?什么场景可以将析构函数覆盖为与编译器所知道的不同的东西?
是的,编译器可以在这种情况下内联虚拟析构函数。让我们考虑一个代码示例:
#include <iostream>
int global = 0;
class A {
public:
virtual void foo() { std::cout << "A" << std::endl; }
virtual ~A() { ++global; }
};
class B : public A {
public:
virtual void foo() { std::cout << "B" << std::endl; }
virtual ~B() { --global; }
};
int main() {
{
B b[5];
b[0].foo();
}
std::cout << "global: " << global << std::endl;
return 0;
}
如您所见,带有 -O3 优化的 clang 3.8 永远不会为 classes 生成代码(带有 -O3 的 gcc 6.1 将生成 class B,但无论如何都会内联析构函数):
main: # @main
pushq %r14
pushq %rbx
pushq %rax
movl std::cout, %edi
movl $.L.str.2, %esi
movl , %edx
callq std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
movq std::cout(%rip), %rax
movq -24(%rax), %rax
movq std::cout+240(%rax), %rbx
testq %rbx, %rbx
je .LBB0_9
cmpb [=11=], 56(%rbx)
je .LBB0_3
movb 67(%rbx), %al
jmp .LBB0_4
.LBB0_3:
movq %rbx, %rdi
callq std::ctype<char>::_M_widen_init() const
movq (%rbx), %rax
movl , %esi
movq %rbx, %rdi
callq *48(%rax)
.LBB0_4: # %_ZNKSt5ctypeIcE5widenEc.exit2
movsbl %al, %esi
movl std::cout, %edi
callq std::basic_ostream<char, std::char_traits<char> >::put(char)
movq %rax, %rdi
callq std::basic_ostream<char, std::char_traits<char> >::flush()
movl std::cout, %edi
movl $.L.str, %esi
movl , %edx
callq std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
movl global(%rip), %esi
movl std::cout, %edi
callq std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
movq %rax, %r14
movq (%r14), %rax
movq -24(%rax), %rax
movq 240(%r14,%rax), %rbx
testq %rbx, %rbx
je .LBB0_9
cmpb [=11=], 56(%rbx)
je .LBB0_7
movb 67(%rbx), %al
jmp .LBB0_8
.LBB0_7:
movq %rbx, %rdi
callq std::ctype<char>::_M_widen_init() const
movq (%rbx), %rax
movl , %esi
movq %rbx, %rdi
callq *48(%rax)
.LBB0_8: # %std::ctype<char>::widen(char) const [clone .exit]
movsbl %al, %esi
movq %r14, %rdi
callq std::basic_ostream<char, std::char_traits<char> >::put(char)
movq %rax, %rdi
callq std::basic_ostream<char, std::char_traits<char> >::flush()
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
retq
.LBB0_9:
callq std::__throw_bad_cast()
pushq %rax
movl std::__ioinit, %edi
callq std::ios_base::Init::Init()
movl std::ios_base::Init::~Init(), %edi
movl std::__ioinit, %esi
movl $__dso_handle, %edx
popq %rax
jmp __cxa_atexit # TAILCALL
global:
.long 0 # 0x0
.L.str:
.asciz "global: "
.L.str.2:
.asciz "B"
Is a compiler allowed to inline a virtual destructor in the case of the implicit destruction of stack allocated objects?
是的。只要它调用对象的正确运行时类型的析构函数。
I understand the 'delete' operation must call the virtual destructor via the virtual function table (e.g. it can't inline), since it can't know the exact class the pointer refers to.
只有当编译器无法知道指针所指的具体类型时,才必须进行虚调用。
But when an object is allocated on the stack, the compiler knows the exact class.
正确。
So I would've thought it would be free to inline the implicit destruction since it can see the actual destructors for the class.
嗯,在栈上分配并不能保证析构函数的定义是可见的。但是如果是可见的,那么你的假设是正确的,编译器可以自由内联。
是否允许编译器在隐式销毁堆栈分配对象的情况下内联虚拟析构函数?
我理解 'delete' 操作必须通过虚函数 table 调用虚析构函数(例如它不能内联),因为它无法知道确切的 class指针指向。
但是当一个对象被分配到栈上时,编译器知道确切的class。所以我认为可以自由地内联隐式破坏,因为它可以看到 class 的实际析构函数。如果不允许编译器这样做,那为什么不呢?什么场景可以将析构函数覆盖为与编译器所知道的不同的东西?
是的,编译器可以在这种情况下内联虚拟析构函数。让我们考虑一个代码示例:
#include <iostream>
int global = 0;
class A {
public:
virtual void foo() { std::cout << "A" << std::endl; }
virtual ~A() { ++global; }
};
class B : public A {
public:
virtual void foo() { std::cout << "B" << std::endl; }
virtual ~B() { --global; }
};
int main() {
{
B b[5];
b[0].foo();
}
std::cout << "global: " << global << std::endl;
return 0;
}
如您所见,带有 -O3 优化的 clang 3.8 永远不会为 classes 生成代码(带有 -O3 的 gcc 6.1 将生成 class B,但无论如何都会内联析构函数):
main: # @main
pushq %r14
pushq %rbx
pushq %rax
movl std::cout, %edi
movl $.L.str.2, %esi
movl , %edx
callq std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
movq std::cout(%rip), %rax
movq -24(%rax), %rax
movq std::cout+240(%rax), %rbx
testq %rbx, %rbx
je .LBB0_9
cmpb [=11=], 56(%rbx)
je .LBB0_3
movb 67(%rbx), %al
jmp .LBB0_4
.LBB0_3:
movq %rbx, %rdi
callq std::ctype<char>::_M_widen_init() const
movq (%rbx), %rax
movl , %esi
movq %rbx, %rdi
callq *48(%rax)
.LBB0_4: # %_ZNKSt5ctypeIcE5widenEc.exit2
movsbl %al, %esi
movl std::cout, %edi
callq std::basic_ostream<char, std::char_traits<char> >::put(char)
movq %rax, %rdi
callq std::basic_ostream<char, std::char_traits<char> >::flush()
movl std::cout, %edi
movl $.L.str, %esi
movl , %edx
callq std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
movl global(%rip), %esi
movl std::cout, %edi
callq std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
movq %rax, %r14
movq (%r14), %rax
movq -24(%rax), %rax
movq 240(%r14,%rax), %rbx
testq %rbx, %rbx
je .LBB0_9
cmpb [=11=], 56(%rbx)
je .LBB0_7
movb 67(%rbx), %al
jmp .LBB0_8
.LBB0_7:
movq %rbx, %rdi
callq std::ctype<char>::_M_widen_init() const
movq (%rbx), %rax
movl , %esi
movq %rbx, %rdi
callq *48(%rax)
.LBB0_8: # %std::ctype<char>::widen(char) const [clone .exit]
movsbl %al, %esi
movq %r14, %rdi
callq std::basic_ostream<char, std::char_traits<char> >::put(char)
movq %rax, %rdi
callq std::basic_ostream<char, std::char_traits<char> >::flush()
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
retq
.LBB0_9:
callq std::__throw_bad_cast()
pushq %rax
movl std::__ioinit, %edi
callq std::ios_base::Init::Init()
movl std::ios_base::Init::~Init(), %edi
movl std::__ioinit, %esi
movl $__dso_handle, %edx
popq %rax
jmp __cxa_atexit # TAILCALL
global:
.long 0 # 0x0
.L.str:
.asciz "global: "
.L.str.2:
.asciz "B"
Is a compiler allowed to inline a virtual destructor in the case of the implicit destruction of stack allocated objects?
是的。只要它调用对象的正确运行时类型的析构函数。
I understand the 'delete' operation must call the virtual destructor via the virtual function table (e.g. it can't inline), since it can't know the exact class the pointer refers to.
只有当编译器无法知道指针所指的具体类型时,才必须进行虚调用。
But when an object is allocated on the stack, the compiler knows the exact class.
正确。
So I would've thought it would be free to inline the implicit destruction since it can see the actual destructors for the class.
嗯,在栈上分配并不能保证析构函数的定义是可见的。但是如果是可见的,那么你的假设是正确的,编译器可以自由内联。