【问题标题】:Order of static destructors静态析构函数的顺序
【发布时间】:2018-05-07 13:58:13
【问题描述】:

如果类Foo 有一个静态成员变量Bar,我希望Bar 的析构函数仅在Foo 的析构函数的最后一个实例运行后运行。下面的代码 sn-p 不会发生这种情况(gcc 6.3,clang 3.8):

#include <memory>
#include <iostream>

class Foo;
static std::unique_ptr<Foo> foo;

struct Bar {
    Bar() {
        std::cout << "Bar()" << std::endl;
    }

    ~Bar() {
        std::cout << "~Bar()" << std::endl;
    }
};

struct Foo {
    Foo() {
        std::cout << "Foo()" << std::endl;
    }

    ~Foo() {
        std::cout << "~Foo()" << std::endl;
    }

    static Bar bar;
};
Bar Foo::bar;

int main(int argc, char **argv) {
    foo = std::make_unique<Foo>();
}

输出:

Bar()
Foo()
~Bar()
~Foo()

为什么这里的破坏顺序不是构造的逆序? 如果~Foo() 使用Foo::bar 这是删除后的使用。

【问题讨论】:

  • 您是否尝试过让~Foo() 使用bar 数据?
  • 是的,这就是我提出这个问题的原因。它不会改变行为,并导致使用被破坏的对象。
  • 语言中没有机制可以确保在所有类实例被销毁后发生某些事情。类不维护其实例的计数器或类似的东西。
  • @n.m.不,但编译器知道在运行Foo() 之前构造Foo::bar(允许在Foo() 中安全使用Foo::bar - 人们希望这种行为也适用于~Foo()
  • 编译器只知道静态对象,它们的销毁顺序与它们的类型无关。

标签: c++


【解决方案1】:

在 C++ 中,对象按出现的顺序构造并以相反的顺序销毁。首先是foo 构造,然后是bar 构造,然后是main,然后bar 被破坏,然后是foo。这是您所看到的行为。出现开关是因为foo 的构造函数没有构造Foo,而是构造了一个空的unique_ptr,因此您在输出中看不到Foo()。然后用输出构造bar,并在main 中创建实际的Foo,在foo 长时间构造之后。

【讨论】:

  • 是的 - 使用 unique_ptr 是为了模仿我在更复杂的应用程序中的行为。因此使用析构函数中的静态类成员是否不安全(即在~Bar() 中使用Foo::bar?这听起来很疯狂。
  • @rthur 一种解决方法是在全局 foo 声明中使用 static std::unique_ptr&lt;Foo&gt; foo = std::make_unique&lt;Foo&gt;();。另一种是在main的末尾写上foo = nullptr;。如果这没有帮助,请说明您想要实现的目标。
  • 感谢您的回答。代码 sn-p 是一个小例子 - 在我的应用程序中 Foo() 需要来自 argv 的参数,并且还需要传递信号,因此是全局的。
  • 您确定 标准 要求某些特定的顺序,尤其是多个翻译单元吗?
  • @BasileStarynkevitch 不同的翻译单位没有订单,所以这不适用。 This answer 有适当的标准引用。
【解决方案2】:

我希望 Bar 的析构函数仅在 Foo 的析构函数的最后一个实例运行后运行。

不,作为static 数据成员,Foo::bar 独立于Foo 的任何实例。

对于你展示的代码,

static std::unique_ptr<Foo> foo; // no Foo created here

Bar Foo::bar; // Foo::bar is initialized before main(), => "Bar()"

int main(int argc, char **argv) {
    foo = std::make_unique<Foo>(); // an instance of Foo is created, => "Foo()"
} 

// objects are destroyed in the reverse order how they're declared
// Foo::bar is defined after foo, so it's destroyed at first => "~Bar()"
// foo is destroyed; the instance of Foo managed by it is destroyed too => "~Foo()"

【讨论】:

  • 那么,全局变量在main() 实际退出之前就被销毁了?看起来不奇怪吗?如果Foo 实例使用static Bar bar 数据怎么办?
  • @Alex 他们在main()之后被销毁了?
  • 让我更清楚一点:~Foo()理论上可以使用static Bar bar
  • @Alex 正是——这就是我提出这个问题的原因。
  • @Alex 然后你会得到一个被破坏的对象。不要使用全局对象是个好主意;或者把Foo::bar的定义移到前面。
【解决方案3】:

这里的复杂之处在于代码没有检测foo 的构造函数。发生的情况是首先构造 foo,然后构造 Foo::bar。对make…unique 的调用构造了一个Foo 对象。然后main 退出,两个静态对象按照构造相反的顺序被销毁:Foo::bar 被销毁,然后foofoo 的析构函数会破坏它指向的 Foo 对象,这是在main 中创建的对象。

【讨论】:

  • foo 似乎并没有首先构建 - Foo::bar 是根据输出。
  • @rthur — foo 的构造函数不写任何输出。构造完成后,它拥有一个空指针。
  • 我可能遗漏了一些东西 - Foo() 打印出 Foo()。即便如此,在Foo() 中使用Foo::bar 会因此是未定义的行为吗?
  • @rthur — foo 的类型是 std::unique_ptr&lt;Foo&gt;。它的默认构造函数不会创建Foo 对象。它包含一个空指针。
  • 啊,是的,抱歉——我以为我们在谈论的是何时构造 Foo 对象,而不是 foo 指针。这解释了构建/销毁的顺序 - 谢谢!
【解决方案4】:

静态对象的生命周期完全基于它们的定义顺序。编译器对何时调用 Bar::Bar() 并不像调用 Bar::~Bar() 一样“知道”。

为了更好地说明问题,考虑这个

class Foo;

struct Bar {
    Bar() {
        std::cout << "Bar()" << std::endl;
    }

    ~Bar() {
        std::cout << "~Bar()" << std::endl;
    }

    void baz() {}
};

struct Foo {
    Foo() {
        bar.baz();
        std::cout << "Foo()" << std::endl;
    }

    ~Foo() {
        std::cout << "~Foo()" << std::endl;
    }

    static Bar bar;
};

Foo foo;
Bar Foo::bar;

int main() {}

打印

Foo()
Bar()
~Bar()
~Foo()

添加std::unique_ptr 会在main 中构造Foo::Foo() 之后推迟它,从而产生编译器“知道”何时调用Bar::Bar() 的错觉。

TLDR 静态对象的定义应晚于其依赖项。在定义bar 之前,定义std::unique_ptr&lt;Foo&gt; 和定义Foo 一样是一个错误

【讨论】:

    【解决方案5】:

    一般来说,您不应该编写依赖于静态(或全局)数据的构造或销毁顺序的代码。这使得代码不可读和不可维护(您可能更喜欢静态智能指针,或者 显式main 调用的初始化或启动例程)。当您链接多个翻译单元时,未指定该顺序。

    注意GCC 提供init_priorityconstructor(有优先级)属性。我相信你应该避免使用它们。但是,__attribute__(constructor)is 有用的inside 插件,用于插件初始化。

    在某些情况下,您还可以使用atexit(3)(至少在 POSIX 系统上)。我不知道这些注册函数是在析构函数之前还是之后调用的(我认为你不应该关心这个顺序)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-05-20
      • 2017-10-20
      • 1970-01-01
      • 2021-08-19
      • 2012-12-26
      相关资源
      最近更新 更多