Skip to content

C++ 单例模式的一种实现

Author: whsu First post: 2025-09-22 22:33 Last modified: 2025-09-22 22:33

cpp单例模式语言特性


参考资料

在 C++ 工程实践中,接触到一个单例模式的实现,比较有意思。直接贴出来:

cpp
class A {
private:
    // private constructor
    A() = default;

    // delete all copy/move constructor and assignment
    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;

public:
    static inline A *GetInstance() {
        static A a;
        return &a;
    }
};

将构造函数声明为 private,并删除拷贝/移动构造和赋值,是单例的基础操作。

巧妙之处在于,这个单例模式利用了[1][2]中提到的特性。当一个局部静态变量被初始化时,多线程的执行环境应当被阻塞,直到变量初始化完成。

Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

编译器一般用互斥锁来实现这种“懒”构造的线程安全性。我们试着编译如下程序:

cpp
// main.cpp

#include <iostream>

void somefun() {
    // pretend to do something...
}

class A {
private:
    // private constructor
    A() {
        somefun();
    }

    // delete all copy/move constructor and assignment
    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;

public:
    static inline A *GetInstance() {
        static A a;
        return &a;
    }

    void HelloWorld() const {
        std::cout << "Hello, World!\n";
    }
};

int main() {
    A::GetInstance()->HelloWorld();
    return 0;
}

利用编译命令:g++ main.cpp -std=c++11 -O0 进行编译,得到 a.out 文件。

我们引入 somefun 的原因是想避免 class A 成为纯值类型,导致不用特别调用构造函数(也就不用加锁)。

我们利用 objdump -d a.out 查看汇编代码,找到 A::GetInstance 的函数体:

00000000000011c8 <_ZN1A11GetInstanceEv>:
    11c8:       f3 0f 1e fa             endbr64
    11cc:       55                      push   %rbp
    11cd:       48 89 e5                mov    %rsp,%rbp
    11d0:       0f b6 05 81 2f 00 00    movzbl 0x2f81(%rip),%eax        # 4158 <_ZGVZN1A11GetInstanceEvE1a>
    11d7:       84 c0                   test   %al,%al
    11d9:       0f 94 c0                sete   %al
    11dc:       84 c0                   test   %al,%al
    11de:       74 36                   je     1216 <_ZN1A11GetInstanceEv+0x4e>
    11e0:       48 8d 05 71 2f 00 00    lea    0x2f71(%rip),%rax        # 4158 <_ZGVZN1A11GetInstanceEvE1a>
    11e7:       48 89 c7                mov    %rax,%rdi
    11ea:       e8 a1 fe ff ff          call   1090 <__cxa_guard_acquire@plt>
    11ef:       85 c0                   test   %eax,%eax
    11f1:       0f 95 c0                setne  %al
    11f4:       84 c0                   test   %al,%al
    11f6:       74 1e                   je     1216 <_ZN1A11GetInstanceEv+0x4e>
    11f8:       48 8d 05 52 2f 00 00    lea    0x2f52(%rip),%rax        # 4151 <_ZZN1A11GetInstanceEvE1a>
    11ff:       48 89 c7                mov    %rax,%rdi
    1202:       e8 a9 ff ff ff          call   11b0 <_ZN1AC1Ev>
    1207:       48 8d 05 4a 2f 00 00    lea    0x2f4a(%rip),%rax        # 4158 <_ZGVZN1A11GetInstanceEvE1a>
    120e:       48 89 c7                mov    %rax,%rdi
    1211:       e8 5a fe ff ff          call   1070 <__cxa_guard_release@plt>
    1216:       48 8d 05 34 2f 00 00    lea    0x2f34(%rip),%rax        # 4151 <_ZZN1A11GetInstanceEvE1a>
    121d:       5d                      pop    %rbp
    121e:       c3                      ret
    121f:       90                      nop

可以看到,在地址 11ea 和 1211 处的语句是加锁解锁的函数调用。中间则为对 class A 构造函数的调用。

笔者测试的平台为 amd64。大约在其他平台上,也是相似的情况吧。

    11ea:       e8 a1 fe ff ff          call   1090 <__cxa_guard_acquire@plt>
    ...
    1211:       e8 5a fe ff ff          call   1070 <__cxa_guard_release@plt>
粤ICP备2024206975号