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

时间:2016-04-25 15:06:33

标签: c++ c++11 singleton static-initialization

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

我试图了解使用更像Meyer Singleton的优势是什么。

例如,可以在头文件中使用:

inline Stream& getStream() { static Stream s; return s; }
static Stream& stream = getStream();

优点是你不必担心引用计数,放置新的,或者有两个类,即代码更简单。由于没有这样做,我确定有一个原因:

  1. 这是不是保证在共享和静态库中只有一个全局对象?似乎ODR应该保证只能有一个静态变量。
  2. 是否有某种性能成本?在我的代码和Nifty Counter中,你似乎都遵循一个引用来访问对象。
  3. 是否存在引用计数实际有用的情况?看起来它仍然会导致构造对象,如果包含头部,并在程序结束时销毁,如Meyer Singleton。
  4. 答案是否涉及手动dlopen?我没有太多的经验。
  5. 编辑:在阅读Yakk的答案时,我被提示编写以下代码,我将其作为快速演示添加到原始问题中。这是一个非常小的例子,展示了如何使用Meyer Singleton +全局引用在main http://coliru.stacked-crooked.com/a/a7f0c8f33ba42b7f之前进行初始化。

4 个答案:

答案 0 :(得分:10)

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

差异如下:

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

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

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

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

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

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

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

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

答案 1 :(得分:3)

总结答案和评论:

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

选项1 - nifty counter pattern,允许使用全局变量

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

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

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

但是,它将在共享对象中创建单例对象的副本,即使所有共享对象和主要与库动态链接也是如此。这是因为Singleton引用变量在头文件中被声明为static,并且必须在编译时将其初始化准备好,无论它在何处使用,包括在共享对象中,在编译期间,在满足它们将被加载到的程序之前。

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

  • 确保创建一次
  • 确保在第一次使用之前
  • 确保在所有共享对象中创建 动态链接 与创建此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"; }

答案 2 :(得分:0)

关于Nifty Counter aka Schwartz Counter的实用性/性能的所有问题基本上都是由Maxim Egorushkin在这个答案中回答的(但另请参见评论主题)。

Global variables in modern C++

主要问题是存在权衡取舍。当你使用Nifty Counter时,你的程序启动时间会慢一些(在大项目中),因为所有这些计数器必须在任何事情发生之前运行。这在迈耶的单身人士身上并没有发生。

但是,在Meyer的单例中,每次要访问全局对象时,都必须检查它是否为null,或者编译器发出的代码检查静态变量是否已经存在在尝试任何访问之前构造。在Nifty Counter中,你已经拥有了你的指针而你只需要开火,因为你可以假设init在启动时发生了。

所以,Nifty Counter与Meyer的单身人士基本上是程序启动时间和运行时间之间的权衡。

答案 3 :(得分:0)

使用此处的解决方案,全局stream变量在静态初始化期间的某个时刻被分配,但是在何时未指定。因此,在静态初始化期间使用来自其他编译单元的stream可能不起作用。 Nifty计数器是一种保证全局(例如std :: cout)即使在静态初始化期间也可用的方法。

#include <iostream>

struct use_std_out_in_ctor
{
    use_std_out_in_ctor()
    {
        // std::cout guaranteed to be initialized even if this
        // ctor runs during static initialization
        std::cout << "Hello world" << std::endl;
    }
};

use_std_out_in_ctor global; // causes ctor to run during static initialization

int main()
{
    std::cout << "Did it print Hello world?" << std::endl;
}