【问题标题】:How static object destruction order is calculated?静态对象销毁顺序如何计算?
【发布时间】:2015-01-13 02:02:24
【问题描述】:

标准要求实现如下:

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/f2\n";
    f1();
    f2();
  } else {
    std::cout << "f2/f1\n";
    f2();
    f1();
  }

  return 0;
}

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

【问题讨论】:

  • 一些编译器的构造函数调用 atexit()。 atexit 保存需要调用的函数列表。列表中项目的顺序是调用 dtor 的顺序。

标签: c++ static


【解决方案1】:

带有-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)以相反的顺序。

【讨论】:

    【解决方案2】:

    标准是怎么说和建议的?

    我们先来看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,有初始化代码(我添加的 cmets):

    ??__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() 的调用,该调用注册生成的函数,即此特定变量的“动态 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
    ...
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-06-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-13
      • 1970-01-01
      • 2019-11-10
      • 1970-01-01
      相关资源
      最近更新 更多