C++单体Singletons设计模式

C++ Singleton design pattern

最近,我遇到了一个C++实现的单体设计模式的实现。它看起来像这样(我从现实生活的例子中采用了它):

1
2
3
4
5
6
7
8
9
10
// a lot of methods are omitted here
class Singleton
{
   public:
       static Singleton* getInstance( );
       ~Singleton( );
   private:
       Singleton( );
       static Singleton* instance;
};

根据这个声明,我可以推断实例字段是在堆上启动的。这意味着存在内存分配。对于我来说,完全不清楚的是什么时候该释放内存?还是有错误和内存泄漏?似乎在实现中存在问题。

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


在2008中,我提供了一个C++ 98的单实例设计模式的实现,它是懒惰的、有保证的破坏,而不是技术上的线程安全:有谁能给我一个C++中的单样本吗?

这里是一个更新的C++ 11实现的单体设计模式,它是懒惰的、正确的破坏和线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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++函数中静态变量的生存期是多少?

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

请参阅这篇文章,解释为什么双重检查锁定在C++上无效。C++程序员应该知道的所有常见的未定义行为是什么?多布斯博士:C++与双重检查锁定的危险:第一部分


作为一个独身者,你通常不希望它被破坏。

当程序终止时,它将被拆散和解除分配,这是单例程序所需要的正常行为。如果您希望能够显式地清除它,那么向类中添加一个静态方法就相当容易了,该方法允许您将其恢复到干净状态,并在下次使用时重新分配它,但这超出了"经典"单例的范围。


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

我更喜欢这种实现(实际上,它没有正确地说我更喜欢,因为我尽量避免使用单例):

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton
{
private:
   Singleton();

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

它没有动态内存分配。


@洛基·阿斯塔里的回答很好。

但是,在有多个静态对象的情况下,您需要确保在所有使用该单例的静态对象不再需要它之前,不会销毁该单例。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() {}
};

另一种非分配的选择是:根据需要创建一个单例,比如类C

1
singleton<C>()

使用

1
2
3
4
5
6
template <class X>
X& singleton()
{
    static X x;
    return x;
}

这也不是C?T?林的答案是自动线程安全在当前的C++,但将在C++0X。


接受的答案中的解决方案有一个显著的缺点——在控件离开main()函数后调用单例的析构函数。当一些依赖对象在main中分配时,可能确实存在问题。

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

这就是为什么我更喜欢堆分配的单例。我为所有的单例提供了一个明确的init()term()方法,并在main中调用它们。因此,我完全控制了单件物品的制造/销毁顺序,并且我保证无论是否有人称为getInstance(),单件物品都将被制造出来。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);


这是一个简单的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#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!
"
; }
    ~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;
}

只创建了一个对象,并且每次在单词后都返回此对象引用。

1
2
3
SingletonClass instance created!
00915CB8
00915CB8

这里,00915cb8是singleton对象的内存位置,在程序运行期间相同,但(通常!)每次运行程序时都不同。

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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


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

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

1
2
3
4
5
6
Singleton * Singleton::getInstance() {
    if (!instance) {
        instance = new Singleton();
    };
    return instance;
};

…然后依靠超出范围的程序进行清理。

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

这样做的另一个问题是它不是线程安全的。在多线程环境中,两个线程都可以在其中一个有机会分配新实例之前通过"if"(如果)。如果你仍然依赖程序终止来清理,这还不算什么大不了的事情。


有人提到过std::call_oncestd::once_flag吗?大多数其他方法——包括双重检查锁定——都被破坏了。

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


除了这里的其他讨论之外,可能值得注意的是,您可以拥有全局性,而不局限于一个实例。例如,考虑引用计数的情况…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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中)的某个地方,可以执行以下操作:

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

引用不需要将指针存储回它们各自的Store,因为这些信息是在编译时提供的。您也不必担心Store的生存期,因为编译器要求它是全局的。如果确实只有一个EDOCX1的实例(1),那么这种方法就没有开销;如果有多个实例,那么编译器就应该对代码生成有足够的了解。如有必要,甚至可以把ItemRef类变成Storefriend(你可以有模板化的朋友!).

如果Store本身是一个模板化的类,那么事情会变得更混乱,但是仍然可以使用这个方法,可能通过实现具有以下签名的助手类:

1
2
3
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>. */
};

用户现在可以为每个全局Store实例创建一个StoreWrapper类型(和全局实例),并始终通过其包装实例访问存储(从而忘记使用Store所需的模板参数的详细信息)。


这是关于对象生命周期管理的。假设您的软件中有多个单例。他们依赖于伐木工人辛格尔顿。在应用程序销毁期间,假设另一个singleton对象使用logger记录其销毁步骤。你必须保证日志记录应该最后清理。因此,请查看本文:http://www.cs.wustl.edu/~schmidt/pdf/objman.pdf


上面链接的文章描述了双重检查锁定的缺点,即在调用对象的构造函数之前,编译器可以为对象分配内存,并设置指向分配内存地址的指针。然而,C++中很容易使用分配器来手动分配内存,然后使用构造调用初始化内存。使用这个appraoch,双重检查锁定工作正常。


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

例子:

1
2
3
4
5
6
7
8
   class CCtrl
    {
    private:
        CCtrl(void);
        virtual ~CCtrl(void);

    public:
        INS(CCtrl);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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

像这样访问您的单身汉:

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


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


如何使用像这样的新位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
class singleton
{
    static singleton *s;
    static unsigned char *buffer[sizeof(singleton)/4 *4] //4 byte align
    static singleton* getinstance()
    {
        if (s == null)
        {
            s = new(buffer) singleton;
        }
        return s;
    }
};