C++ 11 之后的单例模式实现

单例模式(Singleton Pattern)是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。

本文原创,代码和解释都由本人编写,如有发现错误或疏漏请指出,转载请注明出处(lance.moe)。

单例模式的特点

  1. 类构造器私有
  2. 持有自己类的引用
  3. 对外提供获取实例的静态方法

防拷贝构造

1
2
3
4
5
6
7
8
9
class noncopyable {
protected:
    noncopyable() = default;
    ~noncopyable() = default;
    noncopyable(noncopyable &&) = delete;                 // Move construct
    noncopyable(const noncopyable &) = delete;            // Copy construct
    noncopyable &operator=(const noncopyable &) = delete; // Copy assign
    noncopyable &operator=(noncopyable &&) = delete;      // Move assign
};

需要注意的是 C++ 11 开始增加了右值引用语法,很多较为老旧的库没有做移动构造语义的处理,这里已经加上。

单例模式

1
2
3
4
5
6
7
8
template <typename T>
class singleton : public noncopyable {
public:
    static T &instance() {
        static T _instance {};
        return _instance;
    }
};

类内静态方法内的局部静态变量在 C++11 后可保证原子性和线程安全,所以只需要做防拷贝处理即可完成单例。将其制作成模板类,使用时只需要继承 singleton 即可。

实体类

 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
class Client final : public singleton<Client> {
    friend class singleton<Client>;
protected:
    Client();
    ~Client();

public:
    static unsigned long version();
    static void set_version(unsigned long ver);
protected:
    unsigned long _version;
};

Client::Client() : _version(0) {
    // pass
}

Client::~Client() {
    // pass
}

unsigned long Client::version() {
    return instance()._version;
}

void Client::set_version(unsigned long ver) {
    instance()._version = ver;
}
  1. 一个单例模式类要加入 final 来限定不能被继承
  2. 要声明友元类,否则 singleton 基类无法获取实体类的构造函数

完整例子

 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
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>

class noncopyable {
protected:
    noncopyable() = default;
    ~noncopyable() = default;
    noncopyable(noncopyable &&) = delete;                 // Move construct
    noncopyable(const noncopyable &) = delete;            // Copy construct
    noncopyable &operator=(const noncopyable &) = delete; // Copy assign
    noncopyable &operator=(noncopyable &&) = delete;      // Move assign
};

template <typename T>
class singleton : public noncopyable {
public:
    static T &instance() {
        static T _instance;
        return _instance;
    }
};

class Client final : public singleton<Client> {
    friend class singleton<Client>;
protected:
    Client();
    ~Client();

public:
    static unsigned long version();
    static void set_version(unsigned long ver);
protected:
    unsigned long _version;
};

Client::Client() : _version(0) {
    // pass
}

Client::~Client() {
    // pass
}

unsigned long Client::version() {
    return instance()._version;
}

void Client::set_version(unsigned long ver) {
    instance()._version = ver;
}

int main() {
    using namespace std;
    Client::set_version(1919810ul);
    cout << Client::version() << endl; // 输出: 1919810
    const auto &client = Client::instance(); // 正确
    noncopyable test1();               // 编译器报错,noncopyable 没有构造器不能被实体化
    Client client();                   // 编译器报错,Client 构造器被保护不能被实体化
    Client *client = new Client();     // 编译器报错,同上
    return 0;
}

不改造类的情况下使用单例(技巧)

在实际工程中,可能修改一个类的成本比较大,或者是一个基类不方便被修改,那么可以用如下方法使用单例。(只是作为一个技巧,个人不是很推荐,因为无法像上面类继承方案一样做禁止构造操作。很可能出现很多手滑的误用。)

1
2
3
4
5
template <typename T>
T &use() {
    static T _instance {};
    return _instance;
}

这里我们利用模板来制作一个 use 函数。下面给一个用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

template <typename T>
T &use() {
    static T _instance {};
    return _instance;
}

class Foo {
public:
    Foo() {
        puts("I was constructed!");
    }
    void bar() {
        puts("Called bar!");
    }
};

int main() {
    use<Foo>().bar();
    use<Foo>().bar();
    return 0;
}

输出结果:

1
2
3
I was constructed!
Called bar!
Called bar!

可以看到,使用这种方法也可以保证该类只被初始化一次。(前提是不手滑…)

总结

单例模式是一种很基础的设计方式,在面向对象编程流行的时期,单例模式广受批评。

第一点,主要是单例模式不基于接口,对继承、多态不友好。

第二点,在 C++11 普及之前,没有一种能够简单高效靠谱的实现单例模式的方案(大部分方案多多少少都存在一些问题)。

不过现在已经是各门语言里函数式语法满天飞的年代了,在很多 JavaScript 语言的库中,例如 react,编写视图甚至已经开始大力推广FC(Function Components)来代替年事已高的CC(Class Components),时间也证明过度面向对象、过度抽象对一个项目来说反而会降低代码可读性,有些时候不一定是一件好事。

我个人不反对单例,当然这并不意味着单例模式应该被滥用,还是要针对具体情况来定。

以上是个人见解,欢迎补充。

Licensed under CC BY-NC-SA 4.0