C++ 单例模式:这种解决方案好吗?优缺点和替代方案。

5

我正在开发一个C++项目,其中有多个必须是单例的类,并且它们之间存在依赖关系(初始化顺序很重要)。

我想出了以下解决方案:

  1. 我希望成为单例的所有类都具有受保护的构造函数,例如:
class MySingleton1
{
protected:
    MySingleton1();
}
  1. 有一个源文件 singleton_factory.cpp,其中包含一个实例化的类 Singletons,它继承了我想要成为单例的所有类,如下所示:
#include "MySingleton1.hpp"
#include "MySingleton2.hpp"

class Singletons : public MySingleton1, public MySingleton2 {}
static Singletons s_singletons;
singleton_factory.cpp 中,为每种单例类型实现一个 getSingleton 函数的特化。
template<>
MySingleton1& getSingleton()
{
    return s_singletons;
}

template<>
MySingleton2& getSingleton()
{
    return s_singletons;
}
getSingleton的特化将在singleton_factory.hpp中的通用模板变量下“隐藏”:
template <class TSingleton>
TSingleton& getSingleton();

优点:

  • 低耦合性:

    • 单例类不需要“了解”Singletons类,只需要在受保护的限定符下隐藏它们的构造函数(甚至不是强制性的,只是好的做法)。实际上了解Singletons类的唯一代码是singleton_factory.cpp
    • 具体实例瘦依赖: 想要使用类型T的单例的代码,只需包含类型T的头文件和瘦singleton_factory.hpp
  • 初始化顺序可以通过更改Singletons类的继承顺序来控制

  • 没有懒加载 => 线程安全
  • getSingleton()很快,没有dynamic_cast,也没有reinterpret_cast

缺点:

  • 每次出现新的单例类型时,必须将执行相同操作-即"return s_singletons;"的getSingleton专业化添加到singleton_factory.cpp

因此,就我所看到的内容而言,这实际上是相当不错的,所以我想保持这种状态,但我正在征求您的反馈(有什么地方比编程社区更好呢?)。

你看到了这种解决方案的额外优点/缺点是什么?

你建议其他替代方案吗?


我正在开发一个C++项目,其中有多个类必须是单例模式。 它们一定要是单例模式吗?如果不是会发生什么?世界会立即终结吗? - Christopher Pisz
1
@ChristopherPisz 也许你应该写一篇关于为什么单例模式是无用的文章,也许你会成名的 ;) - Zuzu Corneliu
很多人已经这样做了。它并不是完全无用,但已经指出它实际上是一个反模式。https://www.google.com/search?q=singleton+antipattern&rlz=1C1GCEU_enUS821US822&oq=singleton+antipattern&aqs=chrome..69i57j0l3.4263j1j8&sourceid=chrome&ie=UTF-8 - Christopher Pisz
可能有用的链接:https://dev59.com/Q3NA5IYBdhLWcg3wVcJx#1008289 - Galik
对象文件是单例的,命名空间也是单例的 => 只需使用过程式编程。 - Oliv
2个回答

6
这强制使单例集中化,这可能会在更复杂的项目中搞乱依赖项。拥有singleton.cpp的库必须依赖于每个单例所需的全部内容。同时,任何使用单例的人都必须依赖于singleton.cpp库。
基本上,你的代码只能在一个整体的非模块化项目中工作。将其扩展到多个动态库几乎是不可能的。
必须手动维护初始化顺序。
静态全局变量的构造点与main中第一个表达式之前的所有内容的顺序无关。
我用过的一个不错的解决方案是创建一个保存单例内存的动态库。
要成为单例,您需要从CRTP助手继承,它提供了一个::Instance()内联方法。想要使用单例的人使用::Instance()::Instance()创建一个静态局部变量生命周期标记。然后尝试从主DLL获取单例的存储;如果对象已经被创建,它只是将存储转换为对象类型,并增加其引用计数。
如果没有,它将创建新的存储并在其中构造对象。
在静态局部变量生命周期标记销毁时,它会减少引用计数。如果该引用计数达到0,则在当前动态库中局部销毁它。
单例的生存期现在是::Instance()创建变量的生存期的并集。析构发生在非类型擦除代码中,因此我们不必担心包含代码的DLL被卸载。存储是集中的。存储存储的DLL必须比Singleton系统的每个用户都低级,但它反过来没有依赖性,所以这不是一个痛苦。
这远非完美;单例和生命周期是一个不断存在的问题,因为干净的程序关闭很难,并且单例的存在使问题更加困难。但是,在一个相当大的项目中,它已经奏效了。

手动维护初始化顺序对我来说是一个优点,因为它给了我精确的控制,但我想在某些情况下它也可能会让人感到烦恼。将所有单例集中在一个翻译单元中对我来说也更有利,因为它使得从一个翻译单元中了解初始化顺序成为可能。除了这个事实,确实这对于动态库来说是一个问题,这是一个令人信服的论点,让我开始思考。 - Zuzu Corneliu
关于“静态全局变量的构造点与main函数中第一个表达式之前的所有内容无序”-这实际上就是重点,Singletons类是单例类所需排序的启用程序。 - Zuzu Corneliu
3
《C++福音书》第98章第18节说:“程序的干净关闭很难,单例的存在使其更加困难。” 单例常常是问题的源头之一,部分原因在于静态反初始化问题难以编写可靠的单元测试。 - AndyG

0

你的情况可以使用依赖注入吗?例如,让一些序列化代码创建每个类的实例,并将对这些实例的引用传递到需要访问它们的任何其他实例的构造函数中?如果适用,这可能会简化您的设计。理想情况下,您可以将一个单独的、新创建的实例传递到每个构造函数中,以进一步减少耦合,但似乎您需要共享状态。无论如何,也许这会有所帮助。


你能添加任何示例吗?你能提供更多关于你所提出的想法的细节吗? - rudolf_franek
是的,但我建议使用依赖注入,因为它很简单,M. Franek。不应该有太多的移动部件。如果实例数量开始增加,那么通常可以通过重构将它们减少到合理的水平。 - B. Whipple

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接