关于c++:谈-C17-里的-Singleton-模式

49次阅读

共计 6808 个字符,预计需要花费 18 分钟才能阅读完成。

Singleton Pattern

回顾下单件模式,并思考实现一个通用的单件模板类以达成业务端低代码的指标。

Prologue

设计模式中最平民的 Pattern 是哪一个?简直不会有任何一致,那必须是单件模式了。所谓单件模式,是在 C 语言开发历史上经验了各种各样的全局变量失控的折磨后倒退起来的一种技术,得益于 C++ 的封装能力,咱们能够将各种各样的全局变量管控在一个全局动态类(或者说一个类中全都是动态变量的实现形式)中,从而避免任意搁置全局变量带来的灾难性的结果。

我不用在此反复这些结果能有多糟,因为本文不是入门教材,而是实作教训的一个梳理而已。

很显著,动态类只不过是开始,它不是特地好的解决伎俩,因为除了可能集中在一处这一长处之外,这些动态变量还是是予取予求的,此外,晚期(C++11 以前)的编译器没有明确和对立的动态变量初始化程序约定,所以你较难解决这些变量的初始化机会,再一个问题是,没有任何伎俩可能让你实现这些变量的懒加载(lazyinit),除非你将它们通通变成指针,那样的话,还会搞出多少事来只有天知道了。

实践根底

Singleton 模式是 Creational Patterns 中的一种。在 谈 C++17 里的 Factory 模式 中,咱们曾经介绍过创立型模式了,所以本文不再赘述了。

单件模式的用意,及其根本实现,都是非常简单的,因此也不用消耗笔墨凑字数,间接略过。

Goal

咱们想要的是一个可能 lazyinit 或者可能管制起初始化机会的、线程平安的单件模式。

所以上面会介绍 C++11(包含 C++0x)以来的若干可能理论使用的 Singleton 实现计划,但略过了更晚期的实现手法,以及略过了那些顾头不顾尾的示例性实现。嗯,其中一个是这样的:

class singleton {
  static singleton* _ptr;
  singleton() {}
  public:
  singleton* get() {if (_ptr == NULL)
      _ptr = new singleton();
    return _ptr;
  }
};

这个实现最为亲民,因为任何人无需任何常识(还是须要会 c ++ 的)也能一次性手写胜利,甚至不用放心手误或者其它什么编译谬误。它的弱点,较为显著的就先不提了,有时候会被展现者有意无意疏忽或者覆盖的一个重要弱点是,_ptr 是不会被 delete 的:但使用者会压服本人说我的程序就这么一个指针透露,这个代价是付得起的。

可怕吗?或者也不。这就是真的。

考究一点的家伙,晓得 C 提供了一种 atexit 的伎俩,所以会设法挂载一个退出时的 delete 例程,但还是不能解决在 if(_ptr==null) 这里可能产生的跨线程 data racing 问题。问题在于,一个 C++er 你搞个 C 技法在外面,那它也很不纯净的不是?

所以,上面注释开始了。

Meyers’ Singleton in C++

Scott Meyers 是 Effective C++ 系列的作者,他最早提供了简洁版本的 Singletion 模型:

static Singleton& instance() {
  static Singleton instance;
  return instance;
}

“This approach is founded on C++’s guarantee that local static objects are initialized when the object’s definition is first encountered during a call to that function.” … “As a bonus, if you never call a function emulating a non-local static object, you never incur the cost of constructing and destructing the object.”

—— Scott Meyers

Singleton 模式曾经流传了多年,有很多不同目标的实现办法,但 Meyers 的版本是最为精炼且满足线程平安的,它是齐全实用化的。

Backstage

编译器对函数中的动态变量采纳了合乎 C++11 Initialization 个性的初始化和析构绑定,以确保该动态变量将可能在满足 thread-safe 的前提下惟一地被结构和析构。对于确保动态变量初始化和析构的线程平安被细分为多种类型,C++11 保障来有序性,而 C++17 起又反对 Partially-ordered dynamic initialization。当然这么细节的技术规范不用去抠了,就下面的 instance() 中的 instance 变量来说,实际上编译器为此会引入一个暗藏变量来帮忙辨认初始化与否:

static bool __guard = false;
static char __storage[sizeof(Singleton)]; // also align it

Singleton& Instance() {if (!__guard) {
    __guard = true;
    new (__storage) Singleton();}
  return *reinterpret_cast<Singleton*>(__storage);
}

// called automatically when the process exits
void __destruct() {if (__guard)
    reinterpret_cast<Singleton*>(__storage)->~Singleton();}

因为优化的存在,因而编译器经常省略 __guard,间接应用 instance 动态变量,因为该变量在汇编中就是一个指针示意,以非零来代表其 bool 状态即可。因而,以 x86-64 clang 9 为例,Meyers 单例生成的汇编代码如下(godbolt):

singleton_t::instance():            # @singleton_t::instance()
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        cmp     byte ptr [guard variable for singleton_t::instance()::instance], 0
        jne     .LBB1_4
        movabs  rdi, offset guard variable for singleton_t::instance()::instance
        call    __cxa_guard_acquire
        cmp     eax, 0
        je      .LBB1_4
        mov     edi, offset singleton_t::instance()::instance
        call    singleton_t::singleton_t() [base object constructor]
        jmp     .LBB1_3
.LBB1_3:
        movabs  rax, offset singleton_t::~singleton_t() [base object destructor]
        mov     rdi, rax
        movabs  rsi, offset singleton_t::instance()::instance
        movabs  rdx, offset __dso_handle
        call    __cxa_atexit
        movabs  rdi, offset guard variable for singleton_t::instance()::instance
        mov     dword ptr [rbp - 16], eax # 4-byte Spill
        call    __cxa_guard_release
.LBB1_4:
        movabs  rax, offset singleton_t::instance()::instance
        add     rsp, 16
        pop     rbp
        ret

请留神 __cxa_guard_acquire 间接对 instance 起作用,__cxa_guard_release 亦是如此。

Source Code

一个残缺的、采纳 C++11 的 Mayers’ Singleton Pattern 实现是这样的:

#include <stdio.h>

struct singleton_t {
  static
  singleton_t &instance() {
    static singleton_t instance;
    return instance;
  } // instance

  singleton_t(const singleton_t &) = delete;
  singleton_t & operator = (const singleton_t &) = delete;

private:
  singleton_t() {}
  ~singleton_t() {}

public:
  void out(){ printf("out\n"); }
}; // struct singleton_t

int main() {singleton_t::instance().out();
    return 0;
}

基本上说,你复制它,改个类名,就能够开始了。这是大多数 class GlobalVar 的做法。

更多实现办法参考

对于 C++ Singleton Pattern 的更多探讨见诸 于此。

至于说那些弄 double-check 还是别的什么的,都是渣渣。因为 C++11 以来 Singleton 的线程平安问题曾经无需额定的编码思考了。

模板化实现

Mayers 的版本很完满、而且简练扼要,然而有一个问题,那个繁多的实例总是在 main() 开始之前被初始化的,咱们无奈做到 lazyinit。

lazyinit 有什么用呢?

如果咱们打算维持若干单件类,并且它们中很多的 ctor() 还比拟有代价,那么 lazyinit 能够升高启动工夫,并且可能防止运行时从未用到的那些单件类永远也不用被结构出一个实例。

这就须要将 static instance 改为一个 unique_ptr 了。

规范实现

在 hicc-cxx/cmdr-cxx 中,咱们提供了模板化的 singleton<T>,它对于省却代码有很好的帮忙,也能反对线程平安的 lazyinit:

namespace hicc::util {

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

    singleton(const singleton &) = delete;
    singleton &operator=(const singleton) = delete;

    protected:
    struct token {};
    singleton() = default;};

  template<typename T>
  inline T &singleton<T>::instance() {static const std::unique_ptr<T> instance{new T{token{}}};
    return *instance;
  }

} // namespace hicc::util

对于 C++11 规范库的 make_unique_ptr/make_shared_ptr 不能在公有构造函数上工作的问题也早已被多方探讨了,但间接的解决办法没有什么简洁的(而且难以做到跨编译器兼容性),所以在 singleton<T> 中提供了 struct token 的办法来回绝使用者间接结构一个类——为了让派生类可能实现特地的构造函数,struct token {} 以及 ctor() 被标记为 protected。

这个模板类的应用一般来说必须采纳派生类的形式,但须要特地的构造函数:

#include <hicc/hz-common.hh>
// #include <cmdr11/cmdr_common.hh>

class MyVars: public hicc::util::singleton<MyVars> {
  public:
  explicit MyVars(typename hicc::util::singleton<MyVars>::token) {}
  long var1;
};

int main() {printf("%ld\n", MyVars.instance().var1);
}

如果不在意可否手动实例化的编码防备,你能够简化派生类的书写,但模板类中须要去掉 token 的应用。

变参 Singleton 模板

如果你的类须要结构参数,问题就略微简单一点,能够应用咱们的 singleton_with_optional_construction_args,它也是 C++11 起能被间接反对的,无需 hack:

namespace hicc::util {

  template<typename C, typename... Args>
  class singleton_with_optional_construction_args {
    private:
    singleton_with_optional_construction_args() = default;
    static C *_instance;

    public:
    ~singleton_with_optional_construction_args() {
      delete _instance;
      _instance = nullptr;
    }
    static C &instance(Args... args) {if (_instance == nullptr)
        _instance = new C(args...);
      return *_instance;
    }
  };

  template<typename C, typename... Args>
  C *singleton_with_optional_construction_args<C, Args...>::_instance = nullptr;

} // namespace hicc::util

应用办法大略像这样:

void test_singleton_with_optional_construction_args() {int &i = singleton_with_optional_construction_args<int, int>::instance(1);
  UTEST_CHECK(i == 1);

  tester1 &t1 = singleton_with_optional_construction_args<tester1, int>::instance(1);
  UTEST_CHECK(t1.result() == 1);

  tester2 &t2 = singleton_with_optional_construction_args<tester2, int, int>::instance(1, 2);
  UTEST_CHECK(t2.result() == 3);
}

这个实现的原始起源不可考了。

它也不是最优的实现。

其实它能够被改写的和 singleton<T> 类似,不用应用 new 和 delete,然而讲真我从未用到过一个单件类还带结构参数的,也就没有改写的能源了。

留在这里,只是为了提供一个参考范本,我不会倡议你在工程中间接实作它,除非你可能自行欠缺。

Epilogue

严格地说,下面的模板化实现只须要 c++11 反对即可。然而思考到我曾经写了一篇对于 c++ design patterns 的文字了,而我又决定了不关怀 c++11 的个性(老话题了,它比起 17 来不够工程诱惑力),所以还是就冠这个题目名算了。

话题 2:对于 main 之前的初始化

在 Turbo C 年代,main 之前从 OS(例如 DOS/Linux 通过 EXE/PE/ELF)发动一个执行文件的执行动作是通过 c0.asm 来实现的。它接管到 OS 移交到代码执行控制权之后,实现 C 环境的筹备,实现 _atexit 注册的回调函数的注销,而后移交执行控制权到 _main。这个期间里,咱们能够通过指定代码段(_DATA_BSS 之类)的形式来指明一个编译单元中的变量被放在何处,并且通过指明编译程序的形式来控制变量的初始化优先程序。

起初,C++ 的时代,以 Visual C++ 为例,除了实现 C 环境筹备之外,还须要做 CRT 库筹备(c0crt.asm)。VC 的 CRT 库在很大水平上就是它的规范库了。对于 gcc 来说,可能是 libstdc++ 之类。这一段期间里,一些非标扩大甚至容许咱们指定某个动态变量在 CRT 库之前就能被初始化。

在 C++11 之后,main 之前的初始化工作大体上被分为三局部:根本 C 环境,stdc 库,stdc++ 库,如果有必要你能够替换这些外围库,如果你在尝试编写一个 OS 内核,那么通常你都必须替换掉它们。因为 C++ 自身的语法语义能力失去了进一步标准和加强,曾经不再会有也不被举荐你去应用非标扩大了,咱们不再去构想设计某个 singleton 是不是应该在 stdc/stdc++ 之前就必须提前被初始化了。

说到这里,你或者不会了解有何必要提前初始化点,但其实是有的,如果想要接管 stdc 的某些函数入口,或者你须要一个特地的 logging 撑持等等,那就的确会用得上这样的怪招。不过,这的确不是常态,也不会在规模化生产中被利用。

不过就到这里吧,因为很早以前写 singleton 真的是一个横蛮成长的年代啊,所以简略聊聊这些话题。

若文字错乱,查看原文

正文完
 0