【问题标题】:C++ Controlling destructor order for global objectsC++ 控制全局对象的析构函数顺序
【发布时间】:2011-01-11 00:50:45
【问题描述】:

我有一个类 (A),它在其构造函数和析构函数中访问(间接通过静态方法)另一个类 (B) 中的静态变量(STL 容器)。

一个对象可以是全局的、全局常量、另一个类的静态成员、存储在其他类中(它们本身可能有全局或静态实例)或者基本上是 c++ 对象可以存在的任何其他地方。

如果一个 A 对象在 B 中的静态成员之前被构造或在 B 中的静态成员之后被破坏,它会在某些时候导致崩溃(通常是访问冲突)。

是否有某种方法可以保证 A 类的所有实例(除了那些已经泄漏的实例,因为根据定义,那里“丢失”,因此不会以任何方式被破坏)在 B 的静态变量之后构造并在 B 的静态变量之前被破坏?

我已经看到了一些解决方案,用于在另一个之前/之后构造/销毁特定变量,但不是给定类型的所有实例的一般情况,所以我不知道如何解决这个问题。

【问题讨论】:

    标签: c++ memory-management global-variables destructor


    【解决方案1】:

    没有。这被称为static-initialization fiasco。在进入 main 之前构建对象的顺序是未指定的。唯一的保证就是它会发生。

    你可以做的是惰性初始化。这意味着您的对象在您使用它们之前不会被初始化。如:

    struct A { /* some data */ };
    struct B { B(void){ /* get A's data */ } };
    
    A& get_A(void)
    {
        static A instance;
        return instance;
    }
    
    B& get_B(void)
    {
        static B instance;
        return instance;
    }
    

    您使用get_Aget_B 来获取全局实例。 B使用A的部分应该使用get_A,而你使用B的部分应该使用get_B。请注意,get_B 在您的情况下是可选的。

    第一次创建 B 时会发生什么? (全局或函数中)构造函数将调用get_A那是 A 将被创建的地方。这让您可以控制构建事物的顺序。

    注意,我想我把你的 A 和 B 颠倒了。

    【讨论】:

    • 他问的是销毁,而不是初始化。
    • @Neil:销毁顺序是由初始化顺序决定的。
    • 所以我可以将我的“静态容器容器”更改为“静态容器& getContainer(){静态容器容器;返回容器;}”,它会在任何直接或间接调用 getContainer 之后被释放构造函数?如果 A 的实例在程序稍后的某个时间点存储到全局 shared_ptr 中会怎样?
    • 您仍然在自找麻烦,正如之前的 C++ 常见问题解答条目 (parashift.com/c++-faq-lite/ctors.html#faq-10.14) 中提到的关于静态对象与静态指针的关系。
    【解决方案2】:

    一般来说,没有这种方法。不过,有一些解决方法。您可以通过拥有一个全局指针并在 main/WinMain 中初始化/销毁它来获得一个具有全局范围和略小于全局生命周期的对象。此外,您将要销毁的全局状态最后放在一个引用计数的堆对象中。

    另外,考虑重新设计:)

    【讨论】:

      【解决方案3】:

      "Modern C++ Design" 这本书很好地涵盖了这个问题。

      Google 图书包含大部分内容的扫描件 - 请参阅第 6.5 节(第 135 页)-link

      【讨论】:

        【解决方案4】:

        您可以通过将指向对象的指针放在全局空间中来干净地处理这个问题, 然后在你的 main 中按所需顺序更新它们,并在 main 结束时按所需顺序销毁它们。

        【讨论】:

          【解决方案5】:

          正如其他人指出的那样,由于Static initialization order fiasco 问题,没有标准且可移植的方法来解决此问题。

          但是,您应该能够通过应用一些设计来解决您的问题,这样您就可以在一定程度上控制何时(以及如何)构建 A 和 B 的对象。看看像创建模式Singleton 这样的设计模式,它在许多(如果不是大多数)情况下被认为是anti-pattern,尽管它值得学习。另请查看Monostate 模式,它可以用作更好的单例模式。这些模式可以帮助控制对象的创建和生命周期,因此在使用之前会正确初始化。

          一般来说,避免使用全局变量是个好主意 - 坚持使用 deglobalisation 是个好主意。

          【讨论】:

            【解决方案6】:

            如果您使用惰性单例(返回按需创建的静态数据),您最终可能会在一个单例已被删除后使用另一个单例。例如,假设您有一个全局 HttpClient 单例,它允许您发出 http 请求。此外,您可能想要记录,这可能由Logsingleton 提供:

            class HttpClient
            {
                ...
                static HttpClient& singleton()
                {
                    static HttpClient http;
                    return http;
                }
            };
            

            Log 单例也是如此。现在,想象一下 HttpClient 构造函数和析构函数只需记录 HttpClient 已创建和删除。在这种情况下,HttpClient 的析构函数最终可能会使用已删除的 Log 单例。

            Sample code:

            #include <stdio.h>
            
            class Log
            {
                Log()
                {
                    msg("Log");
                }
            
                ~Log()
                {
                    msg("~Log");
                }
            
            public:    
                static Log& singleton()
                {
                    static Log log;
                    return log;
                }
            
                void msg(const char* str)
                {
                    puts(str);
                }
            };
            
            class HttpClient
            {
                HttpClient()
                {
                    Log::singleton().msg("HttpClient");
                }
                ~HttpClient()
                {
                    Log::singleton().msg("~HttpClient");
                }
            
            public:
                static HttpClient& singleton()
                {
                    static HttpClient http;
                    return http;
                }
                void request()
                {
                    Log::singleton().msg("HttpClient::request");
                }
            };
            
            int main()
            {
                HttpClient::singleton().request();
            }
            

            输出是:

             Log
             HttpClient
             HttpClient::request
             ~HttpClient
             ~Log
            

            到目前为止一切都是正确的,仅仅是因为Log是在HttpClient之前构造的,这意味着HttpClient仍然可以在其析构函数中使用Log。现在只需在 HttpClient 构造函数中注释掉日志代码,你就会得到 this output

            Log
            HttpClient::request
            ~Log
            ~HttpClient
            

            如您所见,日志在其析构函数~Log 已被调用后被使用。正如所指出的,去全球化可能是一种更好的方法,但是如果您想使用按需创建的单例并使其中一些比其他的寿命更长,那么您可以制作这样的单例use a global static initialized on demand。我在生产代码中经常使用这种方法:

            class Log
            {
                friend std::unique_ptr<Log>::deleter_type;
                ...
                static std::unique_ptr<Log> log;
            
                static Log& createSingleton()
                {
                    assert(!log);
                    log.reset(new Log);
                    return *log;
                }
            
            public:    
                static Log& singleton()
                {
                    static Log& log = createSingleton();
                    return log;
                }
            };
            
            std::unique_ptr<Log> Log::log;
            

            现在,无论这些单例的构造顺序如何,销毁顺序都将确保 LogHttpClient 之后被销毁。但是,如果从全局静态构造函数中使用 HttpClient,这可能仍然会失败并产生意外的输出。或者,如果您想要多个这样的“超级”全局变量(例如 LogConfig)以随机顺序相互使用,您仍然会遇到这些问题。在这种情况下,有时最好在堆上分配一次,永远不要删除其中一些对象。

            【讨论】:

            • (请注意,我知道这个问题已经有将近十年的历史了,只是想发布示例代码以防万一有人遇到)
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-04-18
            • 1970-01-01
            • 2012-04-10
            • 2018-05-08
            • 2016-03-18
            • 2017-10-20
            相关资源
            最近更新 更多