单例实例作为静态字段与 getInstance() 方法中的静态变量
Singleton instance as static field vs. static variable in getInstance() method
在this thread中,关于单例实例的说法如下:
The static variable can be static to the GetInstance() function, or it can be static in the Singleton class. There's interesting tradeoffs there.
这些取舍是什么?我知道,如果声明为 static
函数变量,则在首次调用该函数之前不会构造单例。我也读过一些关于线程安全的内容,但我不知道这到底意味着什么,或者这两种方法在这方面有何不同。
两者之间还有其他主要区别吗?哪种方法更好?
在我的具体示例中,我有一个工厂 class 设置为单例,我将实例存储为 class 中的 static const
字段。我没有 getInstance()
方法,而是希望用户直接访问实例,如下所示:ItemFactory::factory
。默认构造函数是私有的,实例是静态分配的。
附录:重载 operator()
以调用单例的 createItem()
方法是多么好的想法,这样 Item
可以像这样创建:ItemFactory::factory("id")
?
我投票给静态函数变量。较新的 C++ 标准需要自动线程安全来初始化此类变量。它已经在 GNU C++ 中实现了大约十年。 Visual Studio2015也支持这个。如果你创建一个静态指针变量来保存对你的单例对象的引用,你将不得不手动处理线程问题。
另一方面,如果您创建了一个如下面代码片段所示的静态成员指针字段,您将能够从其他静态方法更改它,也许在处理请求时用其他实例重新初始化此字段更改程序配置。然而,下面的代码片段包含一个错误,只是为了提醒您多线程是多么困难。
class ItemFactory {
static std::atomic_flag initialized = ATOMIC_FLAG_INIT;
static std::unique_ptr<ItemFactory> theFactoryInstance;
public:
static ItemFactory& getInstance() {
if (!initialized.test_and_set(std::memory_order_acquire)) {
theFactoryInstance = std::make_unique<ItemFactory>();
}
return *theFactoryInstance;
}
};
我不建议您将单例实现为在进入 main()
函数之前初始化的全局非指针变量。线程安全问题将随着隐式缓存一致性开销而消失,但您无法以任何精确或可移植的方式控制全局变量的初始化顺序。
无论如何,这个选择不会强制任何永久性的设计影响。由于此实例将位于 class 的 private
部分,因此您可以随时更改它。
我不认为为工厂重载 operator() 是个好主意。 operator()
具有 "execute" 语义,而在工厂中它将代表 "create"。
What are these trade-offs?
这是最重要的考虑因素:
static
数据成员在程序启动时的静态初始化中被初始化。如果任何 static
对象依赖于单例,那么就会有一个 static
initialization order fiasco.
函数局部static
对象在第一次调用函数时被初始化。由于依赖于单例的任何人都会调用该函数,因此单例将被适当地初始化并且不易受到惨败的影响。销毁仍然存在一个非常微妙的问题。如果静态对象的析构函数依赖于单例,但该对象的构造函数不依赖,那么您最终会遇到未定义的行为。
另外,在第一次调用函数时被初始化,意味着函数可以在静态初始化完成并且main
被调用之后被调用。因此,程序可能产生了多个线程。 static
本地的初始化可能存在竞争条件,导致构建多个实例。幸运的是,自 C++11 起,标准保证初始化是线程安全的,并且这种权衡不再存在于符合标准的编译器中。
线程安全不是 static
数据成员的问题。
Which approach is better?
这取决于您的要求以及您支持的标准版本。
What is the best approach to a singleton in c++?
隐藏它是单例的事实并赋予它值语义。
How?
所有单例性都应该是一个实现细节。这样,如果您需要更改实现单例的方式(或者实际上,如果您决定它根本不应该是单例),您的 class 的消费者无需重构他们的程序。
Why ?
因为现在您的程序永远不必担心引用、指针、生命周期等等。它只是使用对象的一个实例,就好像它是一个值一样。安全的是,单例将处理它所具有的任何 lifetime/resource 要求。
What about a singleton that releases resources when not in use?
没问题。
下面是隐藏在具有值语义的对象外观后面的两种方法的示例。
想象一下这个用例:
auto j1 = jobbie();
auto j2 = jobbie();
auto j3 = jobbie();
j1.log("doh");
j2.log("ray");
j3.log("me");
{
shared_file f;
f.log("hello");
}
{
shared_file().log("goodbye");
}
shared_file().log("here's another");
shared_file f2;
{
shared_file().log("no need to reopen");
shared_file().log("or here");
shared_file().log("or even here");
}
f2.log("all done");
其中 jobbie
对象只是单例的外观,但 shared_file
对象在不使用时想要 flush/close 自身。
所以输出应该是这样的:
doh
ray
me
opening file
logging to file: hello
closing file
opening file
logging to file: goodbye
closing file
opening file
logging to file: here's another
closing file
opening file
logging to file: no need to reopen
logging to file: or here
logging to file: or even here
logging to file: all done
closing file
我们可以使用成语来实现这一点,我称之为 'value-semantics-is-a-facade-for-singleton':
#include <iostream>
#include <vector>
// interface
struct jobbie
{
void log(const std::string& s);
private:
// if we decide to make jobbie less singleton-like in future
// then as far as the interface is concerned the only change is here
// and since these items are private, it won't matter to consumers of the class
struct impl;
static impl& get();
};
// implementation
struct jobbie::impl
{
void log(const std::string& s) {
std::cout << s << std::endl;
}
};
auto jobbie::get() -> impl& {
//
// NOTE
// now you can change the singleton storage strategy simply by changing this code
// alternative 1:
static impl _;
return _;
// for example, we could use a weak_ptr which we lock and store the shared_ptr in the outer
// jobbie class. This would give us a shared singleton which releases resources when not in use
}
// implement non-singleton interface
void jobbie::log(const std::string& s)
{
get().log(s);
}
struct shared_file
{
shared_file();
void log(const std::string& s);
private:
struct impl;
static std::shared_ptr<impl> get();
std::shared_ptr<impl> _impl;
};
// private implementation
struct shared_file::impl {
// in a multithreaded program
// we require a condition variable to ensure that the shared resource is closed
// when we try to re-open it (race condition)
struct statics {
std::mutex m;
std::condition_variable cv;
bool still_open = false;
std::weak_ptr<impl> cache;
};
static statics& get_statics() {
static statics _;
return _;
}
impl() {
std::cout << "opening file\n";
}
~impl() {
std::cout << "closing file\n";
// close file here
// and now that it's closed, we can signal the singleton state that it can be
// reopened
auto& stats = get_statics();
// we *must* use a lock otherwise the compiler may re-order memory access
// across the memory fence
auto lock = std::unique_lock<std::mutex>(stats.m);
stats.still_open = false;
lock.unlock();
stats.cv.notify_one();
}
void log(const std::string& s) {
std::cout << "logging to file: " << s << std::endl;
}
};
auto shared_file::get() -> std::shared_ptr<impl>
{
auto& statics = impl::get_statics();
auto lock = std::unique_lock<std::mutex>(statics.m);
std::shared_ptr<impl> candidate;
statics.cv.wait(lock, [&statics, &candidate] {
return bool(candidate = statics.cache.lock())
or not statics.still_open;
});
if (candidate)
return candidate;
statics.cache = candidate = std::make_shared<impl>();
statics.still_open = true;
return candidate;
}
// interface implementation
shared_file::shared_file() : _impl(get()) {}
void shared_file::log(const std::string& s) { _impl->log(s); }
// test our class
auto main() -> int
{
using namespace std;
auto j1 = jobbie();
auto j2 = jobbie();
auto j3 = jobbie();
j1.log("doh");
j2.log("ray");
j3.log("me");
{
shared_file f;
f.log("hello");
}
{
shared_file().log("goodbye");
}
shared_file().log("here's another");
shared_file f2;
{
shared_file().log("no need to reopen");
shared_file().log("or here");
shared_file().log("or even here");
}
f2.log("all done");
return 0;
}
在this thread中,关于单例实例的说法如下:
The static variable can be static to the GetInstance() function, or it can be static in the Singleton class. There's interesting tradeoffs there.
这些取舍是什么?我知道,如果声明为 static
函数变量,则在首次调用该函数之前不会构造单例。我也读过一些关于线程安全的内容,但我不知道这到底意味着什么,或者这两种方法在这方面有何不同。
两者之间还有其他主要区别吗?哪种方法更好?
在我的具体示例中,我有一个工厂 class 设置为单例,我将实例存储为 class 中的 static const
字段。我没有 getInstance()
方法,而是希望用户直接访问实例,如下所示:ItemFactory::factory
。默认构造函数是私有的,实例是静态分配的。
附录:重载 operator()
以调用单例的 createItem()
方法是多么好的想法,这样 Item
可以像这样创建:ItemFactory::factory("id")
?
我投票给静态函数变量。较新的 C++ 标准需要自动线程安全来初始化此类变量。它已经在 GNU C++ 中实现了大约十年。 Visual Studio2015也支持这个。如果你创建一个静态指针变量来保存对你的单例对象的引用,你将不得不手动处理线程问题。
另一方面,如果您创建了一个如下面代码片段所示的静态成员指针字段,您将能够从其他静态方法更改它,也许在处理请求时用其他实例重新初始化此字段更改程序配置。然而,下面的代码片段包含一个错误,只是为了提醒您多线程是多么困难。
class ItemFactory {
static std::atomic_flag initialized = ATOMIC_FLAG_INIT;
static std::unique_ptr<ItemFactory> theFactoryInstance;
public:
static ItemFactory& getInstance() {
if (!initialized.test_and_set(std::memory_order_acquire)) {
theFactoryInstance = std::make_unique<ItemFactory>();
}
return *theFactoryInstance;
}
};
我不建议您将单例实现为在进入 main()
函数之前初始化的全局非指针变量。线程安全问题将随着隐式缓存一致性开销而消失,但您无法以任何精确或可移植的方式控制全局变量的初始化顺序。
无论如何,这个选择不会强制任何永久性的设计影响。由于此实例将位于 class 的 private
部分,因此您可以随时更改它。
我不认为为工厂重载 operator() 是个好主意。 operator()
具有 "execute" 语义,而在工厂中它将代表 "create"。
What are these trade-offs?
这是最重要的考虑因素:
static
数据成员在程序启动时的静态初始化中被初始化。如果任何 static
对象依赖于单例,那么就会有一个 static
initialization order fiasco.
函数局部static
对象在第一次调用函数时被初始化。由于依赖于单例的任何人都会调用该函数,因此单例将被适当地初始化并且不易受到惨败的影响。销毁仍然存在一个非常微妙的问题。如果静态对象的析构函数依赖于单例,但该对象的构造函数不依赖,那么您最终会遇到未定义的行为。
另外,在第一次调用函数时被初始化,意味着函数可以在静态初始化完成并且main
被调用之后被调用。因此,程序可能产生了多个线程。 static
本地的初始化可能存在竞争条件,导致构建多个实例。幸运的是,自 C++11 起,标准保证初始化是线程安全的,并且这种权衡不再存在于符合标准的编译器中。
线程安全不是 static
数据成员的问题。
Which approach is better?
这取决于您的要求以及您支持的标准版本。
What is the best approach to a singleton in c++?
隐藏它是单例的事实并赋予它值语义。
How?
所有单例性都应该是一个实现细节。这样,如果您需要更改实现单例的方式(或者实际上,如果您决定它根本不应该是单例),您的 class 的消费者无需重构他们的程序。
Why ?
因为现在您的程序永远不必担心引用、指针、生命周期等等。它只是使用对象的一个实例,就好像它是一个值一样。安全的是,单例将处理它所具有的任何 lifetime/resource 要求。
What about a singleton that releases resources when not in use?
没问题。
下面是隐藏在具有值语义的对象外观后面的两种方法的示例。
想象一下这个用例:
auto j1 = jobbie();
auto j2 = jobbie();
auto j3 = jobbie();
j1.log("doh");
j2.log("ray");
j3.log("me");
{
shared_file f;
f.log("hello");
}
{
shared_file().log("goodbye");
}
shared_file().log("here's another");
shared_file f2;
{
shared_file().log("no need to reopen");
shared_file().log("or here");
shared_file().log("or even here");
}
f2.log("all done");
其中 jobbie
对象只是单例的外观,但 shared_file
对象在不使用时想要 flush/close 自身。
所以输出应该是这样的:
doh
ray
me
opening file
logging to file: hello
closing file
opening file
logging to file: goodbye
closing file
opening file
logging to file: here's another
closing file
opening file
logging to file: no need to reopen
logging to file: or here
logging to file: or even here
logging to file: all done
closing file
我们可以使用成语来实现这一点,我称之为 'value-semantics-is-a-facade-for-singleton':
#include <iostream>
#include <vector>
// interface
struct jobbie
{
void log(const std::string& s);
private:
// if we decide to make jobbie less singleton-like in future
// then as far as the interface is concerned the only change is here
// and since these items are private, it won't matter to consumers of the class
struct impl;
static impl& get();
};
// implementation
struct jobbie::impl
{
void log(const std::string& s) {
std::cout << s << std::endl;
}
};
auto jobbie::get() -> impl& {
//
// NOTE
// now you can change the singleton storage strategy simply by changing this code
// alternative 1:
static impl _;
return _;
// for example, we could use a weak_ptr which we lock and store the shared_ptr in the outer
// jobbie class. This would give us a shared singleton which releases resources when not in use
}
// implement non-singleton interface
void jobbie::log(const std::string& s)
{
get().log(s);
}
struct shared_file
{
shared_file();
void log(const std::string& s);
private:
struct impl;
static std::shared_ptr<impl> get();
std::shared_ptr<impl> _impl;
};
// private implementation
struct shared_file::impl {
// in a multithreaded program
// we require a condition variable to ensure that the shared resource is closed
// when we try to re-open it (race condition)
struct statics {
std::mutex m;
std::condition_variable cv;
bool still_open = false;
std::weak_ptr<impl> cache;
};
static statics& get_statics() {
static statics _;
return _;
}
impl() {
std::cout << "opening file\n";
}
~impl() {
std::cout << "closing file\n";
// close file here
// and now that it's closed, we can signal the singleton state that it can be
// reopened
auto& stats = get_statics();
// we *must* use a lock otherwise the compiler may re-order memory access
// across the memory fence
auto lock = std::unique_lock<std::mutex>(stats.m);
stats.still_open = false;
lock.unlock();
stats.cv.notify_one();
}
void log(const std::string& s) {
std::cout << "logging to file: " << s << std::endl;
}
};
auto shared_file::get() -> std::shared_ptr<impl>
{
auto& statics = impl::get_statics();
auto lock = std::unique_lock<std::mutex>(statics.m);
std::shared_ptr<impl> candidate;
statics.cv.wait(lock, [&statics, &candidate] {
return bool(candidate = statics.cache.lock())
or not statics.still_open;
});
if (candidate)
return candidate;
statics.cache = candidate = std::make_shared<impl>();
statics.still_open = true;
return candidate;
}
// interface implementation
shared_file::shared_file() : _impl(get()) {}
void shared_file::log(const std::string& s) { _impl->log(s); }
// test our class
auto main() -> int
{
using namespace std;
auto j1 = jobbie();
auto j2 = jobbie();
auto j3 = jobbie();
j1.log("doh");
j2.log("ray");
j3.log("me");
{
shared_file f;
f.log("hello");
}
{
shared_file().log("goodbye");
}
shared_file().log("here's another");
shared_file f2;
{
shared_file().log("no need to reopen");
shared_file().log("or here");
shared_file().log("or even here");
}
f2.log("all done");
return 0;
}