是否可以依赖函数范围的静态变量来执行程序关闭期间调用的方法?

Can I rely on a function-scoped static variable for a method called during program shutdown?

本文关键字:方法 调用 执行程序 变量 依赖 函数 范围 是否 静态      更新时间:2023-10-16

快速上下文:我在程序关闭时看到错误,这些错误源于全局成员之间的依赖关系(::叹息::,我知道,我知道(。一个全局变量的析构函数可能引用另一个全局变量 - 如果那个全局变量已经被破坏,事情就会变得糟糕。

但这里有一个特殊情况,我只是不知道行为是否定义良好:函数中的静态变量。我是否可以依赖函数即使在程序关闭期间也能始终如一地运行?或者是否有可能销毁静态成员,并且函数无论如何都会运行,而不会创建新成员?

这是一个玩具示例,演示了我感兴趣的内容:

class Logger
{
public:
enum class Severity { DEBUG, INFO, WARNING, ERROR };
void Log(Severity sev, const std::string& msg)
{
LogImpl(FormatMessage(sev, msg));
}
Logger() { Log(Severity::INFO, "Logger active"); }
~Logger() { Log(Severity::INFO, "Logger inactive"); }
protected:
static std::string FormatMessage(Severity sev, const std::string& msg)
{
static const std::map<Severity, std::string> enum2str {
{Severity::DEBUG, "DEBUG"},
{Severity::INFO, "INFO"},
{Severity::WARNING, "WARNING"},
{Severity::ERROR, "ERROR"}
};
// Throws or crashes if enum2str is invalid, or uninitialized:
return "[" + enum2str[sev] + "] " + msg;
}
void LogImpl(const std::string& msg)
{
std::cout << msg << std::endl;
}
};

假设我有一个全局的Logger实例。Logger::FormatMessage中的enum2str映射是一个静态变量,因此在程序关闭期间的某个时刻,它将被销毁。

按照标准,这会导致我的程序在关机时崩溃吗?在关机期间,enum2str本质上是不可靠的吗?或者对此进行一些处理 - 例如,如果enum2str在某个时候无效,也许将创建一个新的静态实例?

(我对依赖对象之间的销毁顺序不感兴趣,例如,我声明全局Logger实例。

没有看到更多程序,一般答案是肯定的。该静态映射的销毁可能会导致程序具有未定义的行为:

[基本.开始.术语]

3 如果构造函数的完成或动态初始化 具有静态存储持续时间的对象强烈发生在 另一个,第二个析构函数的完成是有序的 在第一个析构函数启动之前 [...]

4 如果函数包含静态或线程的块范围对象 已销毁且调用函数的存储持续时间 在销毁具有静态或线程存储的对象期间 持续时间,如果控制流,则程序具有未定义的行为 通过先前销毁的块范围的定义 对象。同样,如果块范围对象,则行为未定义 在其销毁后间接使用(即通过指针(。

通常,静态对象的销毁顺序与初始化顺序相反。因此,假设您有一个在记录器中的映射之前提前初始化的静态对象,并且它在自己的析构函数中记录了一些内容,您将获得未定义的行为。

我对依赖对象之间的破坏顺序不感兴趣

您应该这样做,因为这正是确定在程序关闭期间调用FormatMessage是否安全的原因。

在关闭期间运行的代码是静态对象的析构函数,以及向atExit注册的函数。

是否可以依赖函数范围的静态变量来执行程序关闭期间调用的方法?

一般来说,你不能依赖它,但在某些情况下你可以依赖它。

atExit中依赖静态对象是安全的,因此在那里调用FormatMessage是安全的。除非你能保证特定静态对象senum2str之间的破坏顺序,否则在s的析构函数中使用FormatMessage是不安全的。

静态对象保证按其构造的相反顺序被销毁。因此,在销毁构造函数调用FormatMessage的静态对象子集期间,您可以依赖enum2str存在,因为在构造函数中调用FormatMessage可确保在该依赖静态对象完成构造之前构造enum2str

无论销毁顺序如何,依赖静态对象都有一个技巧:永远不要破坏依赖者。这可以通过使用静态函数作用域指针来实现,该指针指向动态分配的对象,您有意从不删除该对象。作为一个缺点,这将触发记忆分析仪中的诊断,并可能增加您的教条同事的血压。