什么是与C ++静态块等效的C ++习惯用法?

我有一个带有一些静态成员的类,我想运行一些代码来初始化它们(假设这段代码不能转换成简单的表达式)。 在Java中,我会这样做

class MyClass { static int myDatum; static { /* do some computation which sets myDatum */ } } 

除非我弄错了,C ++不允许这样的静态代码块,对吧? 我应该做什么呢?

我想要解决以下两个选项:

  1. 进程加载时(或加载此类的DLL时)会发生初始化。
  2. 首次实例化类时会发生初始化。

对于第二种选择,我在考虑:

 class StaticInitialized { static bool staticsInitialized = false; virtual void initializeStatics(); StaticInitialized() { if (!staticsInitialized) { initializeStatics(); staticsInitialized = true; } } }; class MyClass : private StaticInitialized { static int myDatum; void initializeStatics() { /* computation which sets myDatum */ } }; 

但这是不可能的,因为C ++(目前?)不允许初始化非const静态成员。 但是,至少可以通过表达式将静态块的问题减少到静态初始化的问题……

您也可以在C ++中使用静态块 – 外部类。

事实certificate,我们可以实现一个Java风格的静态块,虽然在类之外而不是在它内部,即在翻译单元范围。 实施有点丑陋,但使用时它非常优雅!

用法

如果你写:

 static_block { std::cout << "Hello static block world!" << std::endl; } 

此代码将在main()之前运行。 您可以初始化静态变量或执行您喜欢的任何其他操作。 因此,您可以在类的.cpp实现文件中放置这样的块。

笔记:

  • 必须用大括号括起静态块代码。
  • 在C ++中无法保证静态代码的相对执行顺序。

履行

静态块实现涉及使用函数静态初始化的虚拟变量。 你的静态块实际上是该函数的主体。 为了确保我们不与其他虚拟变量(例如来自另一个静态块 - 或其他任何地方)发生冲突,我们需要一些宏机制。

 #define CONCATENATE(s1, s2) s1##s2 #define EXPAND_THEN_CONCATENATE(s1, s2) CONCATENATE(s1, s2) #ifdef __COUNTER__ #define UNIQUE_IDENTIFIER(prefix) EXPAND_THEN_CONCATENATE(prefix, __COUNTER__) #else #define UNIQUE_IDENTIFIER(prefix) EXPAND_THEN_CONCATENATE(prefix, __LINE__) #endif /* COUNTER */ #else #endif /* UNIQUE_IDENTIFIER */ 

这是将事物放在一起的宏观工作:

 #define static_block STATIC_BLOCK_IMPL1(UNIQUE_IDENTIFIER(_static_block_)) #define STATIC_BLOCK_IMPL1(prefix) \ STATIC_BLOCK_IMPL2(CONCATENATE(prefix,_fn),CONCATENATE(prefix,_var)) #define STATIC_BLOCK_IMPL2(function_name,var_name) \ static void function_name(); \ static int var_name __attribute((unused)) = (function_name(), 0) ; \ static void function_name() 

笔记:

  • 有些编译器不支持__COUNTER__ - 它不是C ++标准的一部分; 在这些情况下,上面的代码使用__LINE__ ,这也适用。 GCC和Clang确实支持__COUNTER__
  • 这是C ++ 98; 你不需要任何C ++ 11/14/17结构。 但是,尽管没有使用任何类或方法,但它不是有效的C.
  • 如果您的C ++ 11编译器不喜欢GCC样式的未使用扩展,则可以删除__attribute ((unused))或替换为[[unused]]
  • 这不会避免或帮助静态初始化顺序失败 ,因为虽然你知道你的静态块将在main()之前执行,但是当相对于其他静态初始化发生这种情况时,你无法保证。

我在实际的编程工作中使用它,所以你可以从这里把它作为.h文件。

对于#1,如果你真的需要初始化进程启动/库加载时,你将不得不使用特定于平台的东西(例如Windows上的DllMain)。

但是,如果它足以让您在执行静态的同一.cpp文件中的任何代码之前运行初始化,则以下内容应该有效:

 // Header: class MyClass { static int myDatum; static int initDatum(); }; 

 // .cpp file: int MyClass::myDatum = MyClass::initDatum(); 

这样,保证在执行该.cpp文件的任何代码之前调用initDatum()

如果您不想污染类定义,还可以使用Lambda (C ++ 11):

 // Header: class MyClass { static int myDatum; }; 

 // .cpp file: int MyClass::myDatum = []() -> int { /*any code here*/ return /*something*/; }(); 

不要忘记最后一对括号 – 实际上是调用lambda。


至于#2,有一个问题:你不能在构造函数中调用虚函数。 你最好在课堂上手工完成,而不是使用它的基类:

 class MyClass { static int myDatum; MyClass() { static bool onlyOnce = []() -> bool { MyClass::myDatum = /*whatever*/; return true; } } }; 

假设该类只有一个构造函数,那就可以正常工作; 它是线程安全的,因为C ++ 11保证了初始化静态局部变量的安全性。

可以在C ++中初始化静态数据成员:

 #include "Bar.h" Bar make_a_bar(); struct Foo { static Bar bar; }; Bar Foo::bar = make_a_bar(); 

您可能不得不考虑跨翻译单元依赖关系,但这是一般方法。

这是使用C ++ 11模拟static块的好方法:

 #define CONCATE_(X,Y) X##Y #define CONCATE(X,Y) CONCATE_(X,Y) #define UNIQUE(NAME) CONCATE(NAME, __LINE__) struct Static_ { template Static_ (T only_once) { only_once(); } ~Static_ () {} // to counter "warning: unused variable" }; // `UNIQUE` macro required if we expect multiple `static` blocks in function #define STATIC static Static_ UNIQUE(block) = [&]() -> void 

用法

 void foo () { std::cout << "foo()\n"; STATIC { std::cout << "Executes only once\n"; }; } 

演示 。

在C ++中没有这样的习语。

原因在于从C ++生成的代码的完全不同的性质:运行时不是“托管”的。 在生成的代码中,在编译之后,不再存在“类”的概念 ,并且没有诸如“类加载器”按需加载的代码实体之类的东西。

有一些元素具有大致相似的行为,但您确实需要准确理解它们的本质才能利用这种行为。

  • 您可以将代码构建到共享库中,该库可以在运行时动态加载。
  • 在C ++ 11中,你可以从类构造函数std :: call_once初始化。 但是,这样的代码将在创建类实例时运行较晚,而不是在加载可执行文件或共享库时运行
  • 您可以使用初始化器定义全局变量和(类)静态变量。 此初始化程序可以是一个函数,它允许您在变量初始化时运行代码。 这些初始化程序的执行顺序仅在单个翻译单元(例如一个*.cpp文件)中很好地定义。

但除此之外,你不能假设任何事情; ESP。 您永远无法确定是否以及何时实际执行此初始化。 这个警告是真实的 。 特别是不要假设这种初始化代码的副作用 。 编译器将这些代码替换为编译器认为“等效”的代码是完全合法的。 在这方面,可以假设未来的编译器版本变得越来越聪明。 您的代码似乎可以工作,但可以打破不同的优化标志,不同的构建过程,更新的编译器版本。


实际提示 :如果你发现自己有几个静态变量,你需要正确初始化,那么你可能想要将它们分解为一个类。 然后,这个类可以有一个常规的构造函数和析构函数来进行初始化/清理。 然后,您可以将该帮助程序类的实例放入单个(类)静态变量中。 C ++为调用类的ctors和dtors提供了非常强的一致性保证,对于任何可以通过官方方式访问的东西(没有强制转换,没有低级欺骗)。

你可能最好采取不同的方法。 实际上是否需要在StaticInitialized中定义静态信息的集合?

考虑创建一个名为SharedData的单独的单例类。 然后,调用SharedData :: Instance()的第一个客户端将触发创建共享数据集合,这将只是普通的类数据,尽管它位于静态分配的单个对象实例中:

// SharedData.h

 class SharedData { public: int m_Status; bool m_Active; static SharedData& instance(); private: SharedData(); } 

// SharedData.cpp

 SharedData::SharedData() : m_Status( 0 ), m_Active( true ) {} // static SharedData& SharedData::instance() { static SharedData s_Instance; return s_Instance; } 

任何对共享数据集感兴趣的客户现在都必须通过SharedData单例访问它,并且第一个调用SharedData :: instance()的客户端将触发在SharedData的ctor中设置该数据,这只会是叫了一次。

现在您的代码表明不同的子类可能有自己的方法来初始化静态数据(通过initializeStatics()的多态性)。 但这似乎是一个相当棘手的想法。 多个派生类是否真的要共享一组静态数据,但每个子类会以不同方式初始化它? 这只是意味着首先构建的类是以自己的狭隘方式设置静态数据的类,然后每个其他类都必须使用此设置。 这真的是你想要的吗?

我也有点混淆为什么你会尝试将多态性与私有inheritance结合起来。 你真正想要使用私有inheritance(而不是组合)的情况非常少。 我想知道你是否认为你需要initializeStatics()是虚拟的,以便派生类能够调用它。 (事实并非如此。)但是你似乎希望覆盖派生类中的initializeStatics(),原因我不清楚(见前面)。 整个设置似乎有点怪异。