14

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

测试.h:

#include <stdio.h>

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

A& getA();

测试.cpp:

#include "test.h"

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

主.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

4 回答 4

9

DLL 的整个概念超出了 C++ 标准的范围。

在 Windows 中,可以在程序执行期间动态卸载 DLL。为了帮助支持这一点,每个 DLL 将处理在加载时构造的静态变量的销毁。结果是静态变量的销毁顺序取决于 DLL 的卸载顺序(当它们收到 DLL_PROCESS_DETACH 通知时)。 DLL 和 Visual C++ 运行时库行为描述了这个过程。

于 2019-02-06T23:13:37.537 回答
4

我看到您的分析中缺少两件事。

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

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

您有两个相关但技术上独立的程序。该标准分别适用于它们中的每一个。C++ 标准不包括独立程序如何相互交互。

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

之前排序:您似乎错过了“之前排序”的定义。是的,输出在“A ctor”之前有“B ctor”。然而,这本身并不意味着 的构造函数在的构造函数b之前排序a。C++ 标准对 [intro.execution] 中的“sequenced before”给出了精确的含义(您引用的标准版本中的第 1.9 节的第 13 条)。使用精确的含义,可以得出结论,如果 的构造函数在的构造函数b之前排序a,那么输出应该在“A ctor”之前有“B ctor”。然而,相反的(你假设的)不成立。

在评论中,您建议将“之前排序”替换为“之前强烈发生”时,这是一个微小的变化。并非如此,因为“之前强烈发生”在标准的较新版本中也具有精确的含义(第6.8.2.1 节 [intro.races] 的第 12条)。事实证明,“强烈发生在之前”是指“在之前排序”或另外三种情况之一。因此,措辞更改是有意扩大标准的该部分,包含比以前更多的案例。

于 2019-02-11T03:19:28.273 回答
2

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

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

通常,当构造函数或析构函数的顺序在库或不同文件中很重要时,有一些简单的技术可以让您这样做。其中之一是使用指向对象的指针。例如,如果对象 A 要求在它之前构造对象 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 被放入列表中,并在最后以特定的顺序被销毁。

测试.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();

测试.cpp

#include "test.h"

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

list<Destructor *> *dList = nullptr;

主文件

#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;
}
于 2019-02-15T02:16:22.113 回答
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和)ab

主.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. a如果and的静态声明b从它们的 getter-Functions 内部移动到模块级别,那么构造函数已经在加载 DLL 时被自动调用。

当然,如果分别在调用 or 之后还在函数ab使用or ,应用程序就会崩溃。main()dlclose(liba)dlclose(libb)

dlopen()如果您正常编译和链接您的应用程序,那么对和的调用dlclose()将由运行时环境中的代码执行。看来,您测试的 Windows 版本按顺序执行这些调用,这出乎您的意料。微软选择这样做的原因可能是,在程序退出时,主应用程序中的任何内容仍然依赖于 DLL 中的任何内容的趋势高于相反的方式。因此,库中的静态对象通常应该在主应用程序被破坏后被破坏。

同样的道理,初始化顺序也应该颠倒:DLLs应该在第一,主应用程序在第二。因此,Linux 在初始化和清理方面都出错了,而 Windows 至少在清理方面是正确的。

于 2019-02-14T12:54:01.710 回答