C++ Nifty Counter习语; 为什么?

Nir*_*man 26 c++ singleton static-initialization c++11

我最近遇到了Nifty Counter Idiom.我的理解是,这用于在标准库中实现全局变量,如cout,cerr等.由于专家选择了它,我认为它是一种非常强大的技术.

我试图了解使用更像Meyer Singleton的东西的优势.

例如,人们可以在头文件中:

inline Stream& getStream() { static Stream s; return s; }
static Stream& stream = getStream();
Run Code Online (Sandbox Code Playgroud)

优点是您不必担心引用计数,放置新的,或者有两个类,即代码更简单.既然没有这样做,我肯定有一个原因:

  1. 这是不是保证在共享和静态库中只有一个全局对象?似乎ODR应该保证只能有一个静态变量.
  2. 是否有某种性能成本?看起来在我的代码和Nifty Counter中,你都遵循一个引用来获取对象.
  3. 是否存在引用计数实际有用的情况?看起来它仍然会导致正在构造的对象,如果包含头部,并在程序结束时销毁,如Meyer Singleton.
  4. 答案是否涉及手动dlopen'ing?我对此没有太多经验.

编辑:在阅读Yakk的答案时,我被提示编写以下代码,我将其作为快速演示添加到原始问题中.这是一个非常小的例子,展示了如何使用Meyer Singleton +全局引用在main之前进行初始化:http://coliru.stacked-crooked.com/a/a7f0c8f33ba42b7f.

Yak*_*ont 10

静态local/Meyer的单例+静态全局引用(您的解决方案)几乎等同于漂亮的计数器.

差异如下:

  1. 解决方案中不需要.cpp文件.

  2. 从技术上讲static Steam&,每个编译单元都存在; 被引用的对象没有.因为在当前版本的C++中无法检测到这一点,所以如果这消失了.但是某些实现可能实际上创建了该引用而不是忽略它.

  3. 有人可以getStream()static Stream&创建之前打电话; 这将导致破坏顺序的困难(流被销毁的时间晚于预期).这可以通过违反规则来避免.

  4. 该标准被强制要求static Streaminline getStream线程中创建本地安全.检测到这种情况不会发生对编译器来说是一个挑战,因此解决方案中可能存在一些冗余的线程安全开销.在漂亮的计数器并没有支持线程安全的明确; 这被认为是安全的,因为它在静态初始化时运行,在预期线程之前.

  5. getStream()必须在每个编译单元中进行调用.只有当它被证明它不能做任何事情时才能进行优化,这很困难.漂亮的计数器具有相似的成本,但是优化或运行时成本的操作可能更简单,也可能不简单.(确定这将需要在各种编译器上检查生成的汇编输出)

  6. 在C++ 11中引入的"魔法静力学"(没有竞争条件的静态本地人).在您的代码中使用C++ 11魔法静态之前可能存在其他问题; 我能想到的唯一一个是getStream()在静态初始化期间直接在另一个线程中调用的人,如上所述(一般来说)应该被禁止.

  7. 在标准领域之外,您的版本将在每个动态链接的代码块(DLL,.so等)中自动且神奇地创建新的单例.漂亮的计数器只会在cpp文件中创建单例.这可能会让图书馆作家更加严格地控制意外产生的新单身; 他们可以将它粘贴到动态库中,而不是产生重复.

避免拥有多个单身有时很重要.

  • @Yakk 我最近尝试了这个,因为我打算写一篇博文。事实证明,即使在 .so 中使用,这也不会(至少在 Linux 上)导致单例的多个副本。这是因为当加载共享库时,它的函数定义 (`getStream`) 只会在尚未定义时加载。因此,全局引用实际上将使用函数的现有定义及其现有的静态局部。 (2认同)

Ami*_*rsh 5

总结答案和评论:

让我们比较库的 3 个不同选项,希望将全局单例作为变量或通过 getter 函数呈现:

选项 1 -漂亮的计数器模式,允许使用全局变量

  • 保证创建一次
  • 保证在第一次使用之前创建
  • 确保在与创建此全局变量的库动态链接的所有共享对象中创建一次。

选项 2 -带有参考变量Meyers 单例模式(如问题中所示):

  • 保证创建一次
  • 保证在第一次使用之前创建

但是,它会在共享对象中创建单例对象的副本,即使所有共享对象和主对象都与库动态链接。这是因为 Singleton 引用变量在头文件中被声明为静态,并且必须在编译时准备好初始化,无论在哪里使用它,包括在共享对象中,在编译时,在遇到它们将被加载到的程序之前。


选项 3 -没有引用变量Meyers 单例模式(调用 getter 来检索 Singleton 对象):

  • 保证创建一次
  • 保证在第一次使用之前创建
  • 确保在与创建此 Singleton 的库动态链接的所有共享对象中创建一次。

然而,在这个选项中没有全局变量也没有内联调用,每次检索单例的调用都是一个函数调用(可以在调用方缓存)。

这个选项看起来像:

// libA .h
struct A {
    A();
};

A& getA();

// some other header
A global_a2 = getA();

// main
int main() {
    std::cerr << "main\n";
}

// libA .cpp - need to be dynamically linked! (same as libstdc++ is...)
// thus the below shall be created only once in the process
A& getA() {
    static A a;
    return a;
} 

A::A() { std::cerr << "construct A\n"; }
Run Code Online (Sandbox Code Playgroud)