如何计算静态对象销毁顺序

How static object destruction order is calculated?

本文关键字:对象 顺序 静态 何计算 计算      更新时间:2023-10-16

该标准要求实现执行以下操作:

3.6.3.1 如果构造函数的完成或动态初始化 具有静态存储持续时间的对象在 另一个,第二个析构函数的完成是有序的 在启动第一个析构函数之前。

以下演示演示了这一点:

struct A
{
  A(int i) :i(i) {}
  ~A() { std::cout << "destruct A(" << i << ")n"; }
  int i;
};
void f1() { static A a(1); } 
void f2() { static A a(2); } 
int main(int argc, char* argv[]) {
  if (argc <= 1) {
    std::cout << "f1/f2n";
    f1();
    f2();
  } else {
    std::cout << "f2/f1n";
    f2();
    f1();
  }
  return 0;
}

问题是:实施如何能够遵守?如何跟踪每个建筑?

斯坦纳德说了什么,建议了什么?

我们先来看看exit()定义

18.5/8:首先,销毁具有线程存储持续时间并与当前线程关联的对象。接下来,具有静态的对象 存储持续时间被销毁,函数通过调用注册 atexit [脚注 221:每次注册函数时都会调用该函数。

然后上面一点我们提醒:

18.5/5:atexit() 函数注册 f 指向的函数,以便在正常程序终止时不带参数调用。

然后查看 3.6.3/3 中终止的操作顺序,我们发现在终止时,向 atexit() 注册的函数以与其注册相反的顺序调用。 这听起来是不是很熟悉?还有更多 !该标准保证了对析构函数的调用和对注册到atexit函数的调用的顺序也以相反的顺序进行。

因此,严格来说,该标准并没有说静态析构函数是使用 atexit 函数管理的,但它表明存在非常强大的联系。 这当然是为什么许多C++实现使用 atexit 机制来破坏静态的原因

如何为特定实现演示这一点?

在您的示例中,很难将

编译器专门为静态销毁生成的代码与终止序列中生成的其他典型代码分开。我向您推荐以下经验:

更改代码以在不同的标头中定义结构,并将成员函数的实现放在不同的文件中。 然后将一个简单的 cpp 文件添加到您的项目中,该文件仅包含全局(即静态存储)变量的定义:

#include "Header.h"  // our declaration for A without implementation 
A a(3); 

使用汇编器输出编译所有代码。 由于在这个编译单元中,只有与 A 的一个实例的构造、初始化和销毁相关的代码,因此很容易理解。

在MSVC 2013中,有初始化代码(我添加的评论):

??__Ea@@YAXXZ PROC                  ; `dynamic initializer for 'a'', COMDAT
; 3    :  A a(3);
...
    push    3                           ; parameter for the intialisation 
    mov ecx, OFFSET ?a@@3UA@@A          ; adress of a
    call    ??0A@@QAE@H@Z               ; call to constructor A::A

为此初始化生成的代码紧跟以下内容(注释来自 MSVC):

     push   OFFSET ??__Fa@@YAXXZ            ; `dynamic atexit destructor for 'a''
     call   _atexit

所以这里写得很清楚! 编译器生成对atexit()的调用,该调用注册生成的函数,即此特定变量的"动态退出析构函数"。 此函数在汇编程序代码的其他位置定义:

??__Fa@@YAXXZ PROC                  ; `dynamic atexit destructor for 'a'', COMDAT
...
    mov ecx, OFFSET ?a@@3UA@@A          ; a        ===> tell which object 
    call    ??1A@@QAE@XZ                ; A::~A    ===> and tell to call destructor
...

而且这个基本的编译单元中几乎没有其他代码。

带有-std=c++11的 GCC 4.8.1 会产生以下f1

push   %rbp
mov    %rsp,%rbp
mov    $0x6021c0,%eax
movzbl (%rax),%eax
test   %al,%al
jne    0x400a1d <f1()+80>
mov    $0x6021c0,%edi
callq  0x400830 <__cxa_guard_acquire@plt>
test   %eax,%eax
setne  %al
test   %al,%al
je     0x400a1d <f1()+80>
mov    $0x1,%esi
mov    $0x6021d0,%edi
callq  0x400b3a <A::A(int)>
mov    $0x6021c0,%edi
callq  0x4008a0 <__cxa_guard_release@plt>
mov    $0x602080,%edx
mov    $0x6021d0,%esi
mov    $0x400b50,%edi
callq  0x400870 <__cxa_atexit@plt>
mov    0x2017ad(%rip),%eax        # 0x6021d0 <_ZZ2f1vE1a>
pop    %rbp
retq   

a的线程安全构造之后,调用__cxa_atexit; %edi指向A的解构器,%esi保存a的地址。退出后,实现使用给定参数调用给定函数,(在这种情况下,它在析构函数中充当this)以相反的顺序。