初始化类模板的静态成员会产生副作用

Initialization of static members of class templates with side effects

本文关键字:副作用 静态成员 初始化      更新时间:2023-10-16

我的C++14应用程序需要动态创建和销毁特定类型的对象。这些对象中的每一个都被命名,并且在构造对象时指定名称。每个名称都硬编码为字符串文字。下面的伪代码演示了这个想法:

int foo()
{
NamedEntity entity1("Bar");
NamedEntity entity2("Baz");
// do some work
return 42;
}

我的目标是创建一个应用程序中使用的所有对象名的常量列表,并使其在运行时可供应用程序访问。

想到的第一个天真的解决方案是对源进行grep,并自动生成一个带有硬编码名称列表的标头。让我们把这个解决方案放在一边,作为最后的选择。

我可以在编译时创建对象名称的列表吗?我非常欣赏Alexanderscu先生的方法,所以我想,"当然,为什么不呢,我要制作一个非常聪明的用户定义字符串文字,并到此为止"。

想法如下:

  1. 使用用户定义的字符串文字为每个对象名实例化一个新类型。我们将把这种类型称为对象名称类型
  2. 为每个实例化的对象名称类型配备一个伪静态成员。使用任意静态函数的返回值初始化所述静态成员。我们将把这个函数称为注册器函数
  3. 使用对象名称列表定义一个单例。使用上面提到的注册器函数将名称附加到单例列表中

用户定义的字符串文字应该大致应用如下:

int foo()
{
NamedEntity entity1("Bar"_probe);
NamedEntity entity2("Baz"_probe);
// do some work
return 42;
}

参见_probe?这应该是解决方案。

好吧,没用。事实证明,编译器将伪静态成员的初始化推迟到实际调用用户定义的字符串文字的时候。这意味着,在每个命名实体至少创建一次之前,我的名称列表将保持不完整。

为什么会这样?显然,注册器函数有副作用(我们需要它只是为了它的副作用),因此,根据cppreference,不能推迟虚拟静态成员的初始化:

延迟的动态初始化

它是实现定义的,无论动态初始化发生在主函数的第一条语句之前(对于静态函数)还是线程的初始函数之前(对于线程局部函数),还是推迟到之后。

如果非内联变量的初始化被推迟到主/线程函数的第一条语句之后进行,则它发生在第一次odr使用任何变量之前,该变量的静态/线程存储持续时间与要初始化的变量在同一转换单元中定义。如果给定的翻译单元中没有变量或函数被odr使用,那么在该翻译单元中定义的非局部变量可能永远不会被初始化(这为按需动态库的行为建模)然而,只要使用了TU中的任何内容,所有初始化或销毁有副作用的非局部变量都将被初始化,即使它们没有在程序中使用

下面你会发现MWE:有所降低

#include <iostream>
int g_foo = 0;
template <typename T, T... Chars>
class Registrator
{
static int dummy_;
public:
static int run()
{
g_foo++;
static constexpr char str[sizeof...(Chars) + 1] = { Chars..., '' };
std::cout << "Registering: " << &str[0] << std::endl;
return g_foo;
}
};
template <typename T, T... Chars>
int Registrator<T, Chars...>::dummy_ = Registrator<T, Chars...>::run();
template <typename T, T... Chars>
inline int operator""_probe()
{
static constexpr char str[sizeof...(Chars) + 1] = { Chars..., '' };
return Registrator<T, Chars...>::run();
}
int main(int argc, char**)
{
std::cout << "g_foo=" << g_foo << std::endl;
if (argc > 1)
{
std::cout << "Hello"_probe << std::endl;
std::cout << "World"_probe << std::endl;
}
std::cout << "g_foo=" << g_foo << std::endl;
return 0;
}

如果它能正常工作,在没有参数的情况下运行它时,您会大致观察到以下输出:

Registering: Hello
Registering: World
g_foo=2
1
2
g_foo=2

然而,我观察到了以下情况:

g_foo=0
g_foo=0

这意味着编译器根本没有初始化伪静态成员

运行至少有一个参数的程序将强制它显式使用用户定义的文字,在这种情况下,伪静态实际上得到了初始化,但初始化被推迟到使用用户定义文字的点:

g_foo=0
Registering: Hello
1
Registering: World
2
g_foo=2

我将GCC 5.4.0与-std=c++14一起使用。为什么编译器不想初始化我的静态?这种行为正确吗?我该如何解决这个问题?

您需要以某种方式实际使用dummy_,例如获取其地址:

static int run()
{
&dummy_;

在联机编译器中运行

还要注意,字符串文字运算符模板是GNU的扩展。一般来说,自我注册可能不是一个好主意。