共享库中静态对象的销毁顺序

时间:2019-02-06 21:34:38

标签: c++ windows singleton shared-libraries object-destruction

我有一个主程序(main.cpp)和一个共享库(test.htest.cpp):

test.h:

#include <stdio.h>

struct A {
    A() { printf("A ctor\n"); }
    ~A() { printf("A dtor\n"); }
};

A& getA();

test.cpp:

#include "test.h"

A& getA() {
    static A a;
    return a;
}

main.cpp:

#include "test.h"

struct B {
    B() { printf("B ctor\n"); }
    ~B() { printf("B dtor\n"); }
};

B& getB() {
    static B b;
    return b;
}

int main() {
    B& b = getB();
    A& a = getA();
    return 0;
}

这是我在Linux上编译这些源代码的方式:

g++ -shared -fPIC test.cpp -o libtest.so
g++ main.cpp -ltest

在Linux上的输出:

B ctor
A ctor
A dtor
B dtor

当我在Windows上运行此示例时(经过一些调整,例如添加dllexport之后),我得到了MSVS 2015/2017:

B ctor
A ctor
B dtor
A dtor

对我来说,第一个输出似乎符合该标准。例如,请参见: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf

从3.6.3.1节开始:

  

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

也就是说,如果首先构造B对象,那么最后应该销毁它-这是我们在Linux上看到的。但是Windows输出是不同的。是MSVC错误还是我缺少什么?

4 个答案:

答案 0 :(得分:9)

DLL的整个概念不在C ++标准的范围之内。

使用Windows,可以在程序执行过程中动态卸载DLL。为了帮助支持这一点,每个DLL都将处理在加载时构造的静态变量的破坏。结果是静态变量将按照依赖于DLL的卸载顺序的顺序被销毁(当它们收到DLL_PROCESS_DETACH通知时)。 DLLs and Visual C++ run-time library behavior描述了此过程。

答案 1 :(得分:4)

我发现您的分析中缺少两件事。

程序:该标准对程序的执行方式提出了要求。您的程序包含命令g++ main.cpp -ltest产生的(可执行)文件,可能是a.outa.exe。特别是,您的程序不包含与其链接的任何共享库。因此,共享库所做的任何事情都超出了标准范围。

好吧,差不多。由于您使用C ++编写了共享库,因此libtest.sotest.dll文件确实属于标准范围,但它本身是独立于调用它的可执行文件的。也就是说,忽略共享库的可观察行为的a.exe的可观察行为必须符合标准,而忽略可执行文件的可观察行为的test.dll的可观察行为必须符合标准。标准。

您有两个相关的但技术上独立的程序。该标准分别适用于它们各自。 C ++标准未涵盖独立程序之间的交互方式。

如果您需要参考,请查看“翻译阶段”的第9条([lex.phases]-您所引用标准的版本中的2.2节)。链接的结果a.out是一个程序映像,而test.dll是执行环境的一部分。

之前排序:您似乎错过了“之前排序”的定义。是的,输出在“ A ctor”之前具有“ B ctor”。但是,这本身并不意味着b的构造函数在a的构造函数之前被排序。 C ++标准为[intro.execution]中的“之前排序”提供了精确的含义(在您所引用的标准版本中,第1.9节的第13节)。使用精确的含义,可以得出结论,如果b的构造函数在a的构造函数之前被排序,则输出应在“ A ctor”之前具有“ B ctor”。 ”。但是,相反(假设的情况)不成立。

在评论中,您建议将“先于先后”替换为“先于先后”,这是一个较小的更改。并非如此,因为“在此之前发生”在标准的较新版本中也具有确切的含义(第6.8.2.1节[intro.races]第clause 12条)。事实证明,“在……之前发生”是指“在……之前发生顺序”或三种其他情况之一。因此,措词的更改是对标准部分的有意扩展,涵盖了比以前更多的案例。

答案 2 :(得分:2)

构造函数和析构函数的相对顺序仅在静态链接的可执行文件或(共享)库中定义。它由作用域的作用域规则和静态对象的顺序定义。后者也很模糊,因为有时很难保证链接的顺序。

共享库(dll)在执行开始时由操作系统加载,也可以由程序按需加载。因此,没有已知的顺序来加载这些库。结果,没有已知的卸载顺序。结果,库之间的构造函数和析构函数的顺序可能会有所不同。在单个库中只能保证它们的相对顺序。

通常,当构造函数或析构函数的顺序在库或不同文件中很重要时,可以使用简单的方法来实现。其中之一是使用指向对象的指针。例如,如果对象A要求在对象B之前构造对象B,则可以执行以下操作:

A *aPtr = nullptr;
class B {
public:
    B() {
      if (aPtr == nullptr) 
         aPtr = new A();
      aPtr->doSomething();
    }
 };
 ...
 B *b = new B();

以上内容将确保在使用A之前先对其进行构造。这样做时,您可以 保留分配对象的列表,或在其他对象中保留指针,shared_pointers等,以安排有序的销毁,例如在退出main之前。

因此,为说明上述内容,我以一种基本方式重新实现了您的示例。肯定有多种处理方式。在此示例中,销毁列表是根据上述技术构建的,分配的A和B放在列表中,并在最后按特定顺序销毁。

test.h

#include <stdio.h>
#include <list>
using namespace std;

// to create a simple list for destructios. 
struct Destructor {
  virtual ~Destructor(){}
};

extern list<Destructor*> *dList;

struct A : public Destructor{
 A() {
  // check existencd of the destruction list.
  if (dList == nullptr)
    dList = new list<Destructor*>();
  dList->push_front(this);

  printf("A ctor\n"); 
 }
 ~A() { printf("A dtor\n"); }
};

A& getA();

test.cpp

#include "test.h"

A& getA() {
    static A *a = new A();;
    return *a;
}

list<Destructor *> *dList = nullptr;

main.cpp

#include "test.h"

struct B : public Destructor {
  B() {
   // check existence of the destruciton list
  if (dList == nullptr)
    dList = new list<Destructor*>();
  dList->push_front(this);

  printf("B ctor\n");
 }
 ~B() { printf("B dtor\n"); }
};

B& getB() {
  static B *b = new B();;
  return *b;
}


int main() {
 B& b = getB();
 A& a = getA();

 // run destructors
 if (dList != nullptr) {
  while (!dList->empty()) {
    Destructor *d = dList->front();
    dList->pop_front();
    delete d;
  }
  delete dList;
 }
 return 0;
}

答案 3 :(得分:1)

即使在Linux上,如果使用dlopen()和dlclose()手动打开和关闭DLL,也会遇到静态构造函数和析构函数调用的冲突:

testa.cpp:

#include <stdio.h>

struct A {
    A() { printf("A ctor\n"); }
    ~A() { printf("A dtor\n"); }
};

A& getA() {
    static A a;
    return a;
}

(testb.cpp是模拟的,除了A更改为B,而a更改为b

main.cpp:

#include <stdio.h>
#include <dlfcn.h>

class A;
class B;

typedef A& getAtype();
typedef B& getBtype();

int main(int argc, char *argv[])
{
    void* liba = dlopen("./libtesta.so", RTLD_NOW);
    printf("dll libtesta.so opened\n");
    void* libb = dlopen("./libtestb.so", RTLD_NOW);
    printf("dll libtestb.so opened\n");
    getAtype* getA = reinterpret_cast<getAtype*>(dlsym(liba, "_Z4getAv"));
    printf("gotten getA\n");
    getBtype* getB = reinterpret_cast<getBtype*>(dlsym(libb, "_Z4getBv"));
    printf("gotten getB\n");
    A& a = (*getA)();
    printf("gotten a\n");
    B& b = (*getB)();
    printf("gotten b\n");

    dlclose(liba);
    printf("dll libtesta.so closed\n");
    dlclose(libb);
    printf("dll libtestb.so closed\n");

    return 0;
}

输出为:

dll libtesta.so opened
dll libtestb.so opened
gotten getA
gotten getB
A ctor
gotten a
B ctor
gotten b
A dtor
dll libtesta.so closed
B dtor
dll libtestb.so closed

有趣的是,a的构造函数的执行推迟到实际调用getA()时执行。 b也是一样。如果将ab的静态声明从其getter-Functions内部移动到模块级别,则在加载DLL时已经自动调用了构造函数。

当然,如果在调用ab之后在main()函数中仍使用dlclose(liba)dlclose(libb),则应用程序将崩溃,分别。

如果您正常编译和链接应用程序,则对dlopen()dlclose()的调用将由运行时环境中的代码执行。看来,您经过测试的Windows版本按顺序执行了这些调用,这是您无法预料的。 Microsoft选择这种方式的原因可能是,在程序退出时,与其他方式相比,主应用程序中的任何内容仍然倾向于依赖DLL中的任何内容的可能性更高。因此,通常应在销毁主应用程序之后销毁库中的静态对象。

以相同的理由,初始化顺序也应颠倒:DLL应该是第一位,主要应用程序是第二位。因此,Linux在初始化和清除上都会出错,而Windows至少在清除上会正确。