C++ 实现单例模式的一个小技巧

单例模式是一种常用的软件设计模式, 它需要保证一个类仅有一个实例, 并提供一个访问它的全局访问点.

通常来说, 单例模式的初始化时机分为两种, 一个是在程序初始化时, 即(饿汉式), 另一个是在第一次使用时(懒汉式).

饿汉式的实现比较简单, 只要定义一个全局静态变量即可, 但是它的缺点是不管你用不用得到这个单例, 它都会在程序初始化时被创建出来, 有可能会造成资源的浪费. 详细代码我就不展开了, 没什么值得注意的地方.

懒汉式(即延迟加载)

所谓懒汉式, 就是在第一次访问时才创建此实例, 后续的访问直接返回此实例即可.

但是, 聪明如你应该意识到, 这里有一个坑需要注意, 即并发安全.

假如有两个线程同时访问这个单例, 且此时单例还未创建, 那么就会导致创建两个实例, 这就违背了单例模式的初衷.

在Java中, 这个问题很好解决, 只要把getInstance()方法加上synchronized关键字即可(当然, 我对Java不是很熟悉, 可能还会有更细粒度的实现方法).

但在C++中, 由于没有Java中的synchronized关键字, 所以需要我们自己来保证线程安全.

到这里, 看来我们得使用锁才能在C++中实现线程安全的单例模式了, 但这里有一个小技巧, 我们可以利用C++11中的局部静态变量来实现线程安全的单例模式, 根据C++11的规定, 局部静态变量的初始化是线程安全的, 且只会在第一次访问时初始化, 之后的访问直接返回此实例即可, 代码如下:

https://stackoverflow.com/questions/8102125/is-local-static-variable-initialization-thread-safe-in-c11
什么? 你用的编译器连C++11也不支持? Respect~

class Singleton
{
public:
    static Singleton& getInstance()
    {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

我们可以使用奇异递归模板的方式, 来让这个单例类更加通用:

template <typename T>
class Singleton
{
public:
    static T& getInstance()
    {
        static T instance;
        return instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

这样, 我们就可以通过继承Singleton类来实现单例模式了, 例如:

class A : public Singleton<A>
{
    friend class Singleton<A>;
    private:
    A() {} // 构造函数私有化
    friend class Singleton<A>; // 为了让Singleton类可以访问A的构造函数
};```

# 限制单例类的实例化
这样做还存在一个问题, 由于我们的构造函数是私有的, 所以Singleton类无法访问, 但是我们又需要让Singleton类能够访问A的构造函数, 所以我们必须在A类中声明Singleton类为友元类.

而在StackExchange上, 有人提出了一个更加优雅的解决方案, 即使用一个"token"

https://codereview.stackexchange.com/questions/173929/modern-c-singleton-template
```c++
template <typename T>
class Singleton
{
public:
    static T& getInstance()
    {
        static T instance{token{}};
        return instance;
    }
private:
    Singleton(Token) {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
protected:
    struct Token {};
};

然后, 在需要实现单例模式的类中, 只需要在构造函数的参数中传入这个token即可, 例如:

class A : public Singleton<A>
{
public:
    A(Token) {}
};

这样, 即使A的构造函数是public, 但是由于外部无法创建这个token, 所以也就无法创建A的实例了.

总结

至此, 我们通过利用C++11中的局部静态变量, 实现了线程安全的单例模式, 并且还可以通过继承Singleton类来实现单例模式, 且不需要将构造函数私有化, 也不需要将Singleton类声明为友元类, 代码更加优雅.

但是需要注意的是, 这样实现的单例模式, 不能引用其他单例. 由于静态变量的销毁是后进先出的, 如果单例的析构函数中引用了其他单例, 那么就可能出现问题.

比如, 有一个A单例和一个B单例, A的析构函数中引用了B. 但是有可能出现这样的情况, 即B先于A被销毁, 这样就会导致A的析构函数中引用了一个已经被销毁的B单例, 从而导致程序崩溃. 但如果我们能够保证A的析构函数中不引用其他单例, 那么这种实现方式就是安全的.

posted @ 2024-01-29 00:23  RiversJin  阅读(31)  评论(0)    收藏  举报