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

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

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

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


当前回答

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

我更喜欢这种实现(事实上,我更喜欢的说法并不正确,因为我尽可能避免单例):

class Singleton
{
private:
   Singleton();

public:
   static Singleton& instance()
   {
      static Singleton INSTANCE;
      return 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所需的模板参数的详细信息)。

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

我更喜欢这种实现(事实上,我更喜欢的说法并不正确,因为我尽可能避免单例):

class Singleton
{
private:
   Singleton();

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

它没有动态内存分配。

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

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

然而,有时使用多个静态对象时,您需要能够保证在所有使用单例的静态对象不再需要它之前,单例不会被破坏。

在这种情况下,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() {}
};

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

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