C ++ Singleton设计模式


734

最近,我碰到了C ++的Singleton设计模式的实现/实现。它看起来像这样(我从现实生活的示例中采用了它):

// a lot of methods are omitted here
class Singleton
{
   public:
       static Singleton* getInstance( );
       ~Singleton( );
   private:
       Singleton( );
       static Singleton* instance;
};

从该声明中,我可以推断出实例字段是在堆上初始化的。这意味着存在内存分配。对我来说,完全不清楚的是何时确切地将要释放内存?还是有错误和内存泄漏?似乎实现中存在问题。

我的主要问题是,如何以正确的方式实施它?



10
您将在本文中找到有关如何实现单例以及C ++中线程安全性的精彩讨论。 aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf

106
@sbi-只有Sith会处理绝对值。没有Singletons就能解决绝大多数问题吗?绝对。单身人士会引起自己的问题吗?是。但是,我不能坦白地说它们很糟糕,因为设计只是考虑折衷和理解您的方法的细微差别。
derekerdmann

11
@derekerdmann:我并不是说您永远不需要全局变量(当您需要一个全局变量时,有时使用Singleton 更好)。我所说的是,应尽可能少地使用它们。Glorifying Singleton是一种有价值的设计模式,给人的印象是使用它很好,而不是它是一种hack,使代码难以理解,难以维护且难以测试。这就是为什么我发表评论。到目前为止,您所说的没有任何矛盾。
2011年

13
@sbi:您说的是“不要使用它们”。后来您更改为“合理使用它们,应尽量减少使用”,这显然不是很合理。
2011年

Answers:


1105

在2008年,我提供了Singleton设计模式的C ++ 98实现,该模式是延迟评估,保证销毁,技术上不是线程安全的:
有人可以为我提供c ++中的Singleton示例吗?

这是Singleton设计模式的更新的C ++ 11实现,该实现是延迟评估,正确销毁和线程安全的

class S
{
    public:
        static S& getInstance()
        {
            static S    instance; // Guaranteed to be destroyed.
                                  // Instantiated on first use.
            return instance;
        }
    private:
        S() {}                    // Constructor? (the {} brackets) are needed here.

        // C++ 03
        // ========
        // Don't forget to declare these two. You want to make sure they
        // are unacceptable otherwise you may accidentally get copies of
        // your singleton appearing.
        S(S const&);              // Don't Implement
        void operator=(S const&); // Don't implement

        // C++ 11
        // =======
        // We can use the better technique of deleting the methods
        // we don't want.
    public:
        S(S const&)               = delete;
        void operator=(S const&)  = delete;

        // Note: Scott Meyers mentions in his Effective Modern
        //       C++ book, that deleted functions should generally
        //       be public as it results in better error messages
        //       due to the compilers behavior to check accessibility
        //       before deleted status
};

请参阅本文,了解何时使用单例:(不常使用)
单例:应如何使用

请参阅有关初始化顺序及其处理方法的这两篇文章:
静态变量初始化顺序
查找C ++静态初始化顺序问题

请参阅描述寿命的文章:
C ++函数中静态变量的寿命是多少?

请参阅本文,该文章讨论对单例的一些线程含义:将单
例实例声明为GetInstance方法的静态变量,它是线程安全的吗?

请参阅这篇文章,解释为什么双重检查锁定在C ++上不起作用:
C ++程序员应该知道哪些常见的未定义行为?
Dobbs博士:C ++和双重检查锁定的风险:第一部分


22
好答案。但要注意,这不是线程安全的stackoverflow.com/questions/1661529/...
伐楼那

4
@zourtney:许多人没有意识到你刚刚做了什么:)
Johann Gerell 2013年

4
@MaximYegorushkin:销毁该销毁的定义非常明确(没有歧义)。请参阅:stackoverflow.com/questions/246564/...
马丁纽约

3
What irks me most though is the run-time check of the hidden boolean in getInstance()这是对实现技术的假设。无需假设它还活着。请参阅stackoverflow.com/a/335746/14065您可以强制执行某种情况,使其始终处于活动状态(开销比少Schwarz counter)。全局变量在初始化顺序(跨编译单元)方面存在更多问题,因为您不强制执行顺序。该模型的优点是1)延迟初始化。2)能够执行命令(Schwarz可以提供帮助,但是比较难看)。是的get_instance()更恶心。
马丁·约克

3
@kol:不,这不是平常的。仅仅因为初学者不加考虑地复制和粘贴代码并不能使其成为通常的代码。您应该始终查看用例,并确保赋值运算符执行预期的操作。复制和粘贴代码将导致您出错。
马丁·约克

47

作为单例,您通常不希望其被破坏。

当程序终止时,它将被拆除并释放,这是单例的正常期望行为。如果您希望能够显式清除它,则可以很容易地在类中添加一个静态方法,该方法允许您将其恢复为干净状态,并在下次使用时重新分配它,但这超出了范围。 “经典”单身人士。


4
如果从不在静态Singleton *实例上显式调用delete,那么从技术上讲,这是否仍不视为内存泄漏?
Andrew Garrison

7
这不仅仅是内存泄漏,而不仅仅是简单声明全局变量。
ilya n。

15
直言不讳...关于单例的“内存泄漏”问题完全是无关紧要的。如果您有状态资源对解构顺序很重要,那么单例可能很危险;但是在程序终止时,操作系统会完全重新获得所有内存...在99.9%的情况下,这完全是学术上的观点。如果您想就什么是“内存泄漏”和什么不是“内存泄漏”来回争论语法,那很好,但是要意识到这是对实际设计决策的干扰。
jkerian 2009年

12
@jkerian:C ++上下文中的内存泄漏和破坏与内存泄漏无关。确实是关于资源控制的。如果您泄漏内存,则不会调用析构函数,因此与该对象关联的任何资源都无法正确释放。内存只是我们在教授编程时所使用的简单示例,但是那里有更多复杂的资源。
马丁·约克

7
@马丁我完全同意你的看法。即使唯一的资源是内存,如果您必须遍历泄漏列表,滤除“无关紧要”的泄漏,仍然会在尝试在程序中查找REAL泄漏时遇到麻烦。最好将所有这些清理干净,以便任何报告泄漏的工具只能报告问题的东西。
海豚

38

您可以避免分配内存。有许多变体,在多线程环境中都存在问题。

我更喜欢这种实现方式(实际上,我没有正确说出我喜欢的方式,因为我尽可能避免单例):

class Singleton
{
private:
   Singleton();

public:
   static Singleton& instance()
   {
      static Singleton INSTANCE;
      return INSTANCE;
   }
};

它没有动态内存分配。


3
在某些情况下,这种惰性初始化不是遵循的理想模式。一个示例是,如果单例的构造函数从堆中分配内存,并且您希望该分配是可预测的,例如在嵌入式系统或其他严格控制的环境中。我更喜欢将Singleton模式用作最佳模式时,将实例创建为类的静态成员。
dma

3
对于许多大型程序,尤其是那些具有动态库的程序。由于库卸载时破坏顺序的问题,在许多平台上退出程序时,任何非原始的全局或静态对象都可能导致段错误/崩溃。这是许多编码约定(包括Google的编码约定)禁止使用非平凡的静态和全局对象的原因之一。
obecalp

在这种实现中,静态实例似乎具有内部链接,并且在不同的翻译单元中将具有唯一且独立的副本,这将导致混乱和错误的行为。但是我看到了很多这样的实现,我缺少什么吗?
FaceBro

是什么阻止用户将其分配给幕后编译器使用其自己的副本构造函数的多个对象?
Tony Tannous

19

@Loki Astari的回答非常好。

但是,有时在多个静态对象中,您需要能够确保单例不会被破坏,直到使用该单例的所有静态对象不再需要它为止。

在这种情况下,即使在程序结束时调用了静态析构函数,也可以使所有用户std::shared_ptr保持单例状态

class Singleton
{
public:
    Singleton(Singleton const&) = delete;
    Singleton& operator=(Singleton const&) = delete;

    static std::shared_ptr<Singleton> instance()
    {
        static std::shared_ptr<Singleton> s{new Singleton};
        return s;
    }

private:
    Singleton() {}
};

9

另一种非分配方式:C根据需要创建一个单例,例如class :

singleton<C>()

使用

template <class X>
X& singleton()
{
    static X x;
    return x;
}

在当前C ++中,此答案和Cătălin的答案都不会自动实现线程安全,但是在C ++ 0x中都不会。


目前在gcc下它是线程安全的(已经存在了一段时间)。
马丁·约克

13
这种设计的问题是如果跨多个库使用。每个库都有该库使用的单例的副本。因此,它不再是单例。
马丁·约克

6

我没有在答案中找到CRTP实现,所以这里是:

template<typename HeirT>
class Singleton
{
public:
    Singleton() = delete;

    Singleton(const Singleton &) = delete;

    Singleton &operator=(const Singleton &) = delete;

    static HeirT &instance()
    {
        static HeirT instance;
        return instance;
    }
};

要使用它,只需继承您的类,例如: class Test : public Singleton<Test>


1
直到我将默认构造函数设置为受保护且'= default;'时,才能使其与C ++ 17一起使用。
WFranczyk

6

接受的答案中的解决方案有一个很大的缺点-单例的析构main()函数在控件离开函数后被调用。当在内部分配一些从属对象时,确实可能存在问题main

尝试在Qt应用程序中引入Singleton时遇到了这个问题。我决定,所有设置对话框都必须为Singletons,并采用上述模式。不幸的是,Qt的主类QApplication是在main函数中的堆栈上分配的,并且当没有应用程序对象可用时,Qt禁止创建/销毁对话框。

这就是为什么我更喜欢堆分配的单例。我为所有单例提供了显式init()term()方法,并在其中调用它们main。因此,我可以完全控制单例的创建/销毁顺序,并且我保证无论是否有人呼叫,都将创建单例getInstance()


2
如果您指的是当前接受的答案,则您的第一句话是错误的。在销毁所有静态存储持续时间对象之前,不会调用析构函数。
马丁·约克

5

这是一个简单的实现。

#include <Windows.h>
#include <iostream>

using namespace std;


class SingletonClass {

public:
    static SingletonClass* getInstance() {

    return (!m_instanceSingleton) ?
        m_instanceSingleton = new SingletonClass : 
        m_instanceSingleton;
    }

private:
    // private constructor and destructor
    SingletonClass() { cout << "SingletonClass instance created!\n"; }
    ~SingletonClass() {}

    // private copy constructor and assignment operator
    SingletonClass(const SingletonClass&);
    SingletonClass& operator=(const SingletonClass&);

    static SingletonClass *m_instanceSingleton;
};

SingletonClass* SingletonClass::m_instanceSingleton = nullptr;



int main(int argc, const char * argv[]) {

    SingletonClass *singleton;
    singleton = singleton->getInstance();
    cout << singleton << endl;

    // Another object gets the reference of the first object!
    SingletonClass *anotherSingleton;
    anotherSingleton = anotherSingleton->getInstance();
    cout << anotherSingleton << endl;

    Sleep(5000);

    return 0;
}

每次之后字后,仅返回一个创建的对象和该对象引用。

SingletonClass instance created!
00915CB8
00915CB8

00915CB8是单例对象的存储位置,在程序运行期间是相同的,但是(通常!)每次运行程序时都不同。

注意:这不是线程安全的。您必须确保线程安全。


5

如果要在堆中分配对象,为什么不使用唯一的指针。由于我们使用唯一的指针,因此内存也将被释放。

class S
{
    public:
        static S& getInstance()
        {
            if( m_s.get() == 0 )
            {
              m_s.reset( new S() );
            }
            return *m_s;
        }

    private:
        static std::unique_ptr<S> m_s;

        S();
        S(S const&);            // Don't Implement
        void operator=(S const&); // Don't implement
};

std::unique_ptr<S> S::m_s(0);


2
这不是线程安全的。更好地使m_s本地staticgetInstance(),并立即对其进行初始化没有一个测试。
Galik

2

它确实可能是从堆中分配的,但是没有源就无法知道。

典型的实现(取自我已经在emacs中拥有的一些代码)将是:

Singleton * Singleton::getInstance() {
    if (!instance) {
        instance = new Singleton();
    };
    return instance;
};

...并依靠程序超出范围进行清理。

如果您在必须手动执行清理的平台上工作,我可能会添加一个手动清理例程。

这样做的另一个问题是它不是线程安全的。在多线程环境中,两个线程都可以通过“ if”,然后每个线程才有机会分配新实例(因此两者都可以)。如果仍然要依靠程序终止来清理,那么这仍然不是什么大问题。


您可以推断出,因为您可以看到实例变量是指向类实例的指针。
Artem Barger 2009年

3
无需动态分配单例。实际上,这是一个坏主意,因为无法使用上述设计自动取消分配。让它超出范围是不调用析构函数而只是懒惰。
马丁·约克2009年

您可以使用atexit函数自动取消分配。那就是我们要做的(不是说这是个好主意)
乔(

2

有没有人提到的std::call_oncestd::once_flag?大多数其他方法-包括双重检查锁定-都是无效的。

单例模式实现中的一个主要问题是安全初始化。唯一安全的方法是使用同步屏障来保护初始化序列。但是,这些障碍本身需要安全地启动。std::once_flag是确保安全初始化的机制。


2

我们最近在EECS课堂上讨论了这个主题。如果要详细查看讲义,请访问http://umich.edu/~eecs381/lecture/IdiomsDesPattsCreational.pdf

我知道有两种正确创建Singleton类的方法。

第一种方式:

实现它的方式类似于您在示例中的实现方式。至于破坏,“ Singletons通常在程序运行的过程中会忍受;大多数OS会在程序终止时恢复内存和大多数其他资源,因此有一种理由不必担心这一点。”

但是,优良作法是在程序终止时进行清理。因此,您可以使用辅助静态SingletonDestructor类来执行此操作,并将其声明为Singleton中的朋友。

class Singleton {
public:
  static Singleton* get_instance();

  // disable copy/move -- this is a Singleton
  Singleton(const Singleton&) = delete;
  Singleton(Singleton&&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  Singleton& operator=(Singleton&&) = delete;

  friend class Singleton_destroyer;

private:
  Singleton();  // no one else can create one
  ~Singleton(); // prevent accidental deletion

  static Singleton* ptr;
};

// auxiliary static object for destroying the memory of Singleton
class Singleton_destroyer {
public:
  ~Singleton_destroyer { delete Singleton::ptr; }
};

Singleton_destroyer将在程序启动时创建,并且“程序终止时,所有全局/静态对象都会被运行时库关闭代码(由链接器插入)破坏,因此the_destroyer将被破坏;其析构函数将删除Singleton,并运行其破坏者。”

第二路

这称为Meyers Singleton,由C ++向导Scott Meyers创建。只需以不同的方式定义get_instance()即可。现在,您还可以摆脱指针成员变量。

// public member function
static Singleton& Singleton::get_instance()
{
  static Singleton s;
  return s;
}

这很干净,因为返回的值是通过引用获得的,您可以使用.语法而不是->访问成员变量。

“编译器会自动构建代码,该代码首先通过声明创建's',而不是随后的声明,然后在程序终止时删除静态对象。”

还要注意,使用Meyers Singleton,“如果对象在终止时相互依赖,则可能会遇到非常困难的情况-Singleton相对于其他对象何时消失?但是对于简单的应用程序,这很好。”


1

除了此处的其他讨论之外,可能值得注意的是,您可以具有全局性,而不会将用法限制为一个实例。例如,考虑引用计数的情况...

struct Store{
   std::array<Something, 1024> data;
   size_t get(size_t idx){ /* ... */ }
   void incr_ref(size_t idx){ /* ... */}
   void decr_ref(size_t idx){ /* ... */}
};

template<Store* store_p>
struct ItemRef{
   size_t idx;
   auto get(){ return store_p->get(idx); };
   ItemRef() { store_p->incr_ref(idx); };
   ~ItemRef() { store_p->decr_ref(idx); };
};

Store store1_g;
Store store2_g; // we don't restrict the number of global Store instances

现在,main您可以在函数中的某处(例如)执行以下操作:

auto ref1_a = ItemRef<&store1_g>(101);
auto ref2_a = ItemRef<&store2_g>(201); 

ref不需要存储指向它们各自的指针,Store因为该信息是在编译时提供的。您也不必担心Store的生存期,因为编译器要求它是全局的。如果确实只有一个实例,Store那么这种方法就没有开销。对于一个以上的实例,取决于编译器对代码生成的明智程度。如果有必要,ItemRef类甚至可以做了friendStore(你可以有模板的朋友!)。

如果Store本身是模板类,则情况会变得更混乱,但仍然可以使用此方法,也许可以通过实现具有以下签名的帮助器类:

template <typename Store_t, Store_t* store_p>
struct StoreWrapper{ /* stuff to access store_p, e.g. methods returning 
                       instances of ItemRef<Store_t, store_p>. */ };

用户现在可以StoreWrapper为每个全局Store实例创建一个类型(和全局实例),并始终通过其包装实例访问商店(从而无需使用模板参数的繁琐细节Store)。


0

这是关于对象生命周期管理的。假设您的软件中有多个单例。它们取决于Logger单例。在应用程序销毁期间,假设另一个单例对象使用Logger记录其销毁步骤。您必须保证Logger应该最后清理。因此,也请查看这篇文章:http : //www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf


0

我的实现类似于Galik的实现。区别在于我的实现允许共享指针清除分配的内存,而不是保持内存直到应用程序退出并清除静态指针。

#pragma once

#include <memory>

template<typename T>
class Singleton
{
private:
  static std::weak_ptr<T> _singleton;
public:
  static std::shared_ptr<T> singleton()
  {
    std::shared_ptr<T> singleton = _singleton.lock();
    if (!singleton) 
    {
      singleton.reset(new T());
      _singleton = singleton;
    }

    return singleton;
  }
};

template<typename T>
std::weak_ptr<T> Singleton<T>::_singleton;

0

您的代码是正确的,除了您没有在class外声明实例指针。静态变量的内部类声明在C ++中不视为声明,但是在其他语言(例如C#Java等)中允许这样做。

class Singleton
{
   public:
       static Singleton* getInstance( );
   private:
       Singleton( );
       static Singleton* instance;
};
Singleton* Singleton::instance; //we need to declare outside because static variables are global

您必须知道Singleton实例不需要由我们手动删除。我们在整个程序中只需要一个对象,因此在程序执行结束时,它将自动释放。


-1

上面链接到该论文的文章描述了双重检查锁定的缺点,即在调用对象的构造函数之前,编译器可能会为对象分配内存并设置指向所分配内存地址的指针。在c ++中,使用分配器手动分配内存,然后使用构造调用初始化内存非常容易。使用此方法,双重检查锁定就可以正常工作。


2
不幸的是没有。一些最好的C ++开发人员对此进行了深入的讨论。在C ++ 03中,双重检查锁定被打破。
马丁·约克

-1
#define INS(c) private:void operator=(c const&){};public:static c& I(){static c _instance;return _instance;}

例:

   class CCtrl
    {
    private:
        CCtrl(void);
        virtual ~CCtrl(void);

    public:
        INS(CCtrl);

-1

简单的单例类,这必须是您的头文件

#ifndef SC_SINGLETON_CLASS_H
#define SC_SINGLETON_CLASS_H

class SingletonClass
{
    public:
        static SingletonClass* Instance()
        {
           static SingletonClass* instance = new SingletonClass();
           return instance;
        }

        void Relocate(int X, int Y, int Z);

    private:
        SingletonClass();
        ~SingletonClass();
};

#define sSingletonClass SingletonClass::Instance()

#endif

像这样访问您的单身人士:

sSingletonClass->Relocate(1, 2, 5);

-3

我认为您应该编写一个静态函数,其中删除了您的静态对象。当您要关闭应用程序时,应调用此函数。这将确保您没有内存泄漏。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.