如果您使用惰性单例(返回按需创建的静态数据),您最终可能会在一个单例已被删除后使用另一个单例。例如,假设您有一个全局 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;
现在,无论这些单例的构造顺序如何,销毁顺序都将确保 Log 在 HttpClient 之后被销毁。但是,如果从全局静态构造函数中使用 HttpClient,这可能仍然会失败并产生意外的输出。或者,如果您想要多个这样的“超级”全局变量(例如 Log 和 Config)以随机顺序相互使用,您仍然会遇到这些问题。在这种情况下,有时最好在堆上分配一次,永远不要删除其中一些对象。