【问题标题】:How to delay destruction of static object (logger) in C++?如何延迟 C++ 中静态对象(记录器)的销毁?
【发布时间】:2021-09-19 11:44:55
【问题描述】:

我有一个类,它将应用程序的记录器保存为 unique_ptr。记录器可以通过静态函数进行设置。此外,显然可以记录消息。我省略了任何线程同步(互斥体)以使事情变得更容易。

class LoggerWrapper {
 public:
  static void SetLogger(std::unique_ptr<logger::ILogger> new_logger);

  static void Log(const std::string& message);

 private:
  static std::unique_ptr<logger::ILogger> logger_;
};
void LoggerWrapper::Log(const std::string& message) {
  if (!logger_) {
    // cannot log
  } else {
    logger_->OnLogEvent(message);
  }
}

void LoggerWrapper::SetLogger(std::unique_ptr<logger::ILogger> new_logger) {
  logger_ = std::move(new_logger);
}

我的问题是:unique_ptr 在应用程序中的其他一些类之前被破坏。 例如。是Class Foo 的 DTOR 想要记录一些东西,unique_ptr 可能已经被销毁(目前就是这种情况)。这会导致 ILogger 实现被破坏,导致无法输出日志。

有人知道如何轻松解决此问题吗?我需要以某种方式“延迟”静态 unique_ptr 的破坏。我也尝试将其更改为 shared_ptr,但这只会导致 SIGABRT 出现“调用纯虚拟方法”错误。

提前致谢!

编辑:创建了一个与我的经验相矛盾的最小工作示例。在这种情况下,静态记录器比 Foo 类寿命更长。

EDIT2:我的应用程序使用exit。这似乎改变了破坏的顺序。

EDIT3:exit 不会 破坏本地对象。

/******************************************************************************

                              Online C++ Compiler.
               Code, Compile, Run and Debug C++ program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class ILogger {
  public:
  ILogger() {
      std::cout << "ILogger CTOR" << std::endl;
  }
  ~ILogger() {
      std::cout << "ILogger DTOR" << std::endl;
  }
  
  virtual void OnLogEvent(const std::string& log_message) {
        std::cout << "OnLogEvent: " << log_message << std::endl;
  }
};

class LoggerWrapper {
 public:
  static void SetLogger(std::unique_ptr<ILogger> new_logger) {
  logger_ = std::move(new_logger);
}

  static void Log(const std::string& message) {
  if (!logger_) {
    // cannot log
  } else {
    logger_->OnLogEvent(message);
  }
};

 private:
  static std::unique_ptr<ILogger> logger_;
};

class Foo {
  public:
   Foo(const std::string& name) : name_{name} {
       LoggerWrapper::Log(name_ + ": CTOR");
   }
   ~Foo() {
       LoggerWrapper::Log(name_ + ": DTOR");
   }
  private:
   std::string name_;
};

// declaring logger_ first causes it to be deleted AFTER foo
std::unique_ptr<ILogger> LoggerWrapper::logger_;
std::unique_ptr<Foo> foo;


int main()
{
    LoggerWrapper::SetLogger(std::make_unique<ILogger>());
    foo = std::make_unique<Foo>("Global FOO");
    
    // local variables do NOT get destroyed when calling exit!
    auto foo_local = Foo("Local FOO");

    exit(1);
}

【问题讨论】:

  • 静态对象的破坏顺序与构造顺序相反。使用静态存储时长来控制事物的构建顺序并不总是那么容易。如果可能的话,我会完全摆脱对订单的依赖。也许将日志从析构函数移动到 before_exit 方法或其他东西。
  • @super 感谢您的回答。是的,我正在考虑改用Shutdown 方法。尽管如此,我还是很难找到另一个解决方案:D
  • @super 我以前听说过逆向构造顺序。但这仅适用于静态变量,对吗?例如。静态变量A是在静态变量B之前构造的,所以在它之后就被销毁了。是否有任何规则可以确定静态成员(例如我的记录器)是否在另一个使用它的非静态类(例如Foo)之前/之后被破坏?
  • This 可能有点相关,虽然有点老了。
  • 是的,具有非静态存储持续时间的东西总是会在任何静态对象被破坏之前超出范围。

标签: c++ pointers logging static destructor


【解决方案1】:

这是微不足道的。

首先你不使用全局静态对象(你不应该使用这样的全局状态)。您使用函数静态对象,因此您可以控制创建/销毁的顺序。

所以改变这个:

std::unique_ptr<ILogger> LoggerWrapper::logger_;
std::unique_ptr<Foo> foo;

进入:

 class GlobalLogger
 {
     public:
         ILogger& getLogger() {
             static ILogger  logger;  // if you must use unique_ptr you can do that here
             return logger;           // But much simpler to use a normal object.
         }
  };
  class GlobalFoo
  {
      public:
          Foo& getFoo() {
              // If there is a chance that foo is going to 
              // use global logger in its destructor
              // then it should simply call `GlobalLogger::getLogger()`
              // in the constructor of Foo. You then 
              // guarantee the order of creation and thus destruction.
              // Alternatively, you can call it here in thus
              // function just before the declaration of foo.
              static Foo foo;
              return foo;
          }
  };

  // Where you were using `logger_` use `GlobalLogger::getLogger()`
  // Where you were using `foo`     use `GlobalFoo::getFoo()`

如果我们使用您的原始代码作为起点,我们可以这样做:

#include <iostream>
#include <memory>
#include <string>

// Please don't do this.
// This is the number one worst practice.
// https://stackoverflow.com/questions/1452721/why-is-using-namespace-std-considered-bad-practice
using namespace std;

class ILogger {
    public:
        ILogger() {
            std::cout << "ILogger CTOR" << std::endl;
        }
        ~ILogger() {
            std::cout << "ILogger DTOR" << std::endl;
        }

        virtual void OnLogEvent(const std::string& log_message) {
            std::cout << "OnLogEvent: " << log_message << std::endl;
        }
};

class LoggerWrapper
{
    // Here store the logger
    // as a static member of this private function.
    // The the SetLogger() Log() functions get this reference.
    static std::unique_ptr<ILogger>& getLogReference() {
        static std::unique_ptr<ILogger> logger;
        return logger;
    }

    public:
        static void SetLogger(std::unique_ptr<ILogger> new_logger) {
            // Save the new reference.
            getLogReference() = std::move(new_logger);
        }

        // Use the logger if it has been set.
        static void Log(const std::string& message) {
            std::unique_ptr<ILogger>& logger_ = getLogReference();
            if (!logger_) {
                // cannot log
            } else {
                logger_->OnLogEvent(message);
            }
        };
};

class Foo {
    public:
        Foo(const std::string& name) : name_{name} {
            // This calls Log()
            // Which calls getLogReference()
            // Which forces the creation of the function static
            // variable logger so it is created before this
            // object is fully initialized (if it has not already
            // been created).
            // 
            // This means this object was created after the logger
            LoggerWrapper::Log(name_ + ": CTOR");
        }
        ~Foo() {
            // Because the Log() function was called in the
            // constructor we know the loger was fully constructed first
            // thus this object will be destroyed first
            // so the logger object is guaranteed to be
            // available in this objects destructor
            // so it is safe to use.
            LoggerWrapper::Log(name_ + ": DTOR");
        }
    private:
        std::string name_;
};


std::unique_ptr<Foo>& globalFoo() {
    // foo may destroy an object created later
    // that has a destructor that calls LoggerWrapper::Log()
    // So we need to call the Log function here before foo
    // is created.
    LoggerWrapper::Log("Initializing Global foo");
    // Note: Unless somebody else has explicitly called SetLogger()
    // the above line is unlikely to log anything as the logger
    // will be null at this point.

    static std::unique_ptr<Foo> foo;
    return foo;
}

int main()
{
    LoggerWrapper::SetLogger(std::make_unique<ILogger>());
    globalFoo() = std::make_unique<Foo>("Global FOO");

    // local variables do NOT get destroyed when calling exit!
    auto foo_local = Foo("Local FOO");

    exit(1);
}

【讨论】:

  • 但这还是有析构顺序问题。我认为记录器对象永远不应该被破坏。
  • @prehistoricpenguin 不,它没有。如果您认为要在析构函数中使用 Global Logger,那么您只需在构造函数中调用 GlobalLogger::getLogger()。这保证了对象首先被构造,保证了它会在这个对象之后被销毁。
  • 关于您对使用unique_ptr 的评论:在我的应用程序中,ILogger 是一个抽象类,它阻止我使用引用。此外,记录器应该是可交换的,如最小工作示例 (SetLogger) 所示。这将如何适用于您提出的解决方案?
  • @MartinYork 进一步澄清这一点(最小的工作示例):有一个包装类LoggerWrapper 持有一个静态记录器ILogger。通过调用包装器的静态Log 方法,应用程序可以通过静态记录器记录消息。可以使用包装器SetLogger 方法设置/交换静态记录器。
  • @prehistoricpenguin 如果您遵循最佳实践方法,即没有文件范围静态存储持续时间对象并将它们全部移动到文件范围静态存储持续时间对象中,则无需强制在构造函数中创建。然后根据需要懒惰地构造它们,您可以通过在 main 中首先创建记录器来强制首先创建记录器。
【解决方案2】:

静态(全局)对象的创建和销毁顺序未定义。这给您留下了一些选择。

  1. 不要使用全局对象,只使用原始指针,您可以按照正确的顺序销毁自己。
  2. 使用指向永远不会销毁的记录器的指针。这从技术上讲是内存泄漏,但内核会在您的应用退出时进行清理。
  3. 为您的记录器使用shared_ptr。每个使用记录器的对象都会获得一个shared_ptr,并且记录器将在最后一个对象被销毁后被清理。

【讨论】:

  • 第一句话太宽泛了。在同一个翻译单元中定义的静态对象的构造顺序就是它们的定义顺序。在不同翻译单元中定义的静态对象的构造顺序是未指定的(不是未定义;这是一个不同的类别)。静态对象的破坏顺序与构造顺序相反(是的,运行时必须跟踪什么时候构造了什么才能做到这一点)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-13
  • 1970-01-01
  • 1970-01-01
  • 2019-03-17
  • 2019-11-10
  • 2019-06-30
相关资源
最近更新 更多