具有副作用的 class 模板静态成员的初始化

Initialization of static members of class templates with side effects

我的 C++14 应用程序需要动态创建和销毁特定类型的 object。这些 object 中的每一个都被命名,并且在构造 object 时分配名称。每个名称都是 hard-coded 作为字符串文字。以下伪代码演示了这个想法:

int foo()
{
    NamedEntity entity1("Bar");
    NamedEntity entity2("Baz");

    // do some work

    return 42;
}

我的objective是为应用程序中使用的所有object名称创建一个常量列表,并让应用程序可以访问运行次。

想到的第一个天真的解决方案是 grep 源并自动生成 header 和 hard-coded 名称列表。让我们把这个解决方案放在一边作为最后的选择。

我可以在编译时创建 object 名称 的列表吗?我非常钦佩 Alexanderscu 先生的方式,所以我想,"Sure, why not, I'm going to craft a really clever user-defined string literal and call it a day"。

当时的想法如下:

  1. 使用 user-defined 字符串文字为每个 object 名称 实例化一个新类型。我们将此类型称为 object 名称类型 .
  2. 为每个实例化的 object 名称类型 配备一个虚拟静态成员。使用任意静态函数的 return 值初始化所述静态成员。我们将此函数称为 注册器函数
  3. 使用 object 个名称 的列表定义单例。使用上面提到的注册函数将名称附加到单例列表。

user-defined 字符串文字应该大致应用如下:

int foo()
{
    NamedEntity entity1("Bar"_probe);
    NamedEntity entity2("Baz"_probe);

    // do some work

    return 42;
}

看到_probe了吗?这应该是解决方案。

嗯,没用。事实证明,编译器将 虚拟静态成员 的初始化推迟到实际调用 user-defined 字符串文字的位置。这意味着我的名称列表将保持不完整,直到每个命名实体至少创建一次。

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

Deferred dynamic initialization

It is implementation-defined whether dynamic initialization happens-before the first statement of the main function (for statics) or the initial function of the thread (for thread-locals), or deferred to happen after.

If the initialization of a non-inline variable is deferred to happen after the first statement of main/thread function, it happens before the first odr-use of any variable with static/thread storage duration defined in the same translation unit as the variable to be initialized. If no variable or function is odr-used from a given translation unit, the non-local variables defined in that translation unit may never be initialized (this models the behavior of an on-demand dynamic library). However, as long as anything from a TU is odr-used, all non-local variables whose initialization or destruction has side effects will be initialized even if they are not used in the program.

下面您会发现 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..., '[=13=]' };
        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..., '[=13=]' };
    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

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

运行 至少有一个参数的程序将强制它显式使用 user-defined 文字,在这种情况下,虚拟静态实际上得到了初始化,但初始化被推迟到使用 user-defined 文字的地方:

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

我在 -std=c++14 中使用 GCC 5.4.0。为什么编译器不想初始化我的静态变量?这种行为是否正确?我该如何解决这个问题?

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

static int run()
{
    &dummy_;

Run in online compiler

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