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

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

从这个声明中,我可以推断实例字段是在堆上启动的。这意味着存在内存分配。对我来说完全不清楚的是,内存将在何时被释放?或者是否存在错误和内存泄漏?似乎在实现方面存在问题。

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


当前回答

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

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类甚至可以成为Store的朋友(您可以有模板朋友!)。

如果Store本身是一个模板化的类,那么事情就变得更糟了,但是仍然可以使用这个方法,也许可以通过实现具有以下签名的helper类:

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所需的模板参数的详细信息)。

其他回答

有人提到过std::call_once和std::once_flag吗?大多数其他方法——包括双重检查锁定——都是失败的。

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

作为单身汉,你通常不希望它被破坏。

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

我想在这里展示C++中的另一个单例。使用模板编程是有意义的。此外,从不可复制和不可移动的类派生出单例类是有意义的。下面是代码中的样子:

#include<iostream>
#include<string>

class DoNotCopy
{
protected:
    DoNotCopy(void) = default;
    DoNotCopy(const DoNotCopy&) = delete;
    DoNotCopy& operator=(const DoNotCopy&) = delete;
};

class DoNotMove
{
protected:
    DoNotMove(void) = default;
    DoNotMove(DoNotMove&&) = delete;
    DoNotMove& operator=(DoNotMove&&) = delete;
};

class DoNotCopyMove : public DoNotCopy,
    public DoNotMove
{
protected:
    DoNotCopyMove(void) = default;
};

template<class T>
class Singleton : public DoNotCopyMove
{
public:
    static T& Instance(void)
    {
        static T instance;
        return instance;
    }

protected:
    Singleton(void) = default;
};

class Logger final: public Singleton<Logger>
{
public:
    void log(const std::string& str) { std::cout << str << std::endl; }
};



int main()
{
    Logger::Instance().log("xx");
}

拆分为NotCopyable和NotMovable类允许您更具体地定义单例(有时您希望移动单个实例)。

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

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类甚至可以成为Store的朋友(您可以有模板朋友!)。

如果Store本身是一个模板化的类,那么事情就变得更糟了,但是仍然可以使用这个方法,也许可以通过实现具有以下签名的helper类:

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所需的模板参数的详细信息)。

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