C++静态初始化顺序:添加到映射中

12

我们无法确定静态对象初始化的顺序。

但是在以下示例中是否存在问题?

  • 一个静态变量是映射(或其他容器)
  • 从另一个静态变量,我们填充该映射

代码:

class Factory
{
public:
    static bool Register(name, func);

private:
    static map<string, func> s_map;
};

// in cpp file
map<string, func> Factory::s_map;

bool Factory::Register(name, func)
{
    s_map[name] = func;
}

并且在另一个cpp文件中

static bool registered = Factory::Register("myType", MyTypeCreate);

当我注册更多类型时,我不依赖于容器中的顺序。但是对于容器中的第一个添加呢?我能确定它已经被“足够”初始化以接受第一个元素吗?

或者这是“静态初始化顺序惨败”的另一个问题吗?

2个回答

9

您的情形不能保证按预期工作。成功取决于链接顺序。

一种确保方法是通过一个(静态)函数访问地图,该函数将对象创建为静态变量,例如:

h文件:

class Factory
{
public:
    static void Register(string, func);

private:
    static map<string, func>& TheMap();
};

cpp文件:

map<string, func>& Factory::TheMap()
{
    static map<string, func> g_;
    return g_;
}

void Factory::Register(string name, func f)
{
    TheMap()[name] = f;
}

这样做的缺点是,静态变量的销毁顺序很难由开发者控制。在地图的情况下这没问题。但如果静态变量相互引用,"静态链接混乱"会更糟:就我的经验而言,与程序启动时相比,在程序结束时防止/调试崩溃要困难得多。

编辑,2022-09-01:我修复了代码中的问题(来自问题)。现在它将在正确的上下文中编译(此处未包含)。


这样在一个翻译单元中可以正常工作,但当静态变量分布在多个单元时可能会失败。听起来很合理。 - fen
因此,当您通过静态函数访问此类映射时,我们保证它在第一次使用之前已创建。 - fen
@fen 我认为它可以在不同的编译单元中工作。当然,如果您的 registered 变量不是静态的而是具有外部链接,则会出现问题... - Wolf
3
静态变量的销毁顺序未被定义。它是:按照创建的相反顺序进行销毁。 - Jarod42
1
@Jarod42 好吧,我过于简化了。问题在于当初始化顺序取决于实际运行时属性时,很难控制它。我重新表述了一下。 - Wolf
对于进一步的读者:实际上,这是在https://isocpp.org/wiki/faq/ctors#static-init-order-on-first-use中概述的解决方案。然而,不是使用静态对象,而是使用静态指针(由于在https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2下描述的去初始化问题)。 - damb

6

由于我有点懒,所以这里复制了http://en.cppreference.com/的内容:

非局部变量

所有具有静态存储期的非局部变量都在程序启动时初始化,在主函数执行之前(除非延迟执行,见下文)。

...

动态初始化

在所有静态初始化完成后,以下情况会发生非局部变量的动态初始化:

...

延迟动态初始化

动态初始化是否发生在主函数的第一条语句(对于静态变量)或线程的初始函数(对于线程局部变量)之前,或者延迟到之后是由实现定义的。

如果非内联变量的初始化被延迟到主/线程函数的第一条语句之后进行,则它会在与要初始化的变量在同一个翻译单元中定义的任何具有静态/线程存储期限制的变量的第一个odr-use之前发生。

重要的部分是 odr-use

ODR-use

非正式地说,如果读取其值(除非它是编译时常量)或写入其值,获取其地址或将引用绑定到它,则对象被odr-used;

由于s_map是通过Factory::Register填充的,所以我在这里没有看到问题。


如果地图的实现非常简单,甚至可以作为静态初始化/编译时的一部分进行初始化。但是即使初始化被延迟,只要它们在同一个翻译单元中,在Factory::Register中使用之前将会被初始化。然而,如果地图在一个翻译单元中定义,而Factory::Register在另一个翻译单元中定义,可能会发生任何事情。

所以,地图总是首先初始化,因为它是静态初始化的一部分(零初始化)。然后s_registered是动态初始化的一部分(它调用一个方法)- 所以它会正常工作……这是正确的吗? - fen
1
@fen 看起来地图可能在第一次注册调用之前初始化,但这取决于编译器是否支持区分。就我所看到的,可以使用没有内联构造函数的映射实现,那么编译器应该如何决定它是否被平凡地初始化?我发现这个参考很难记住,并且不得不处理甚至不符合C++11标准的编译器,因此我甚至试图避免被迫使用我的答案中显示的单例方法。 - Wolf
1
如果您使用默认构造函数声明一个类的对象,它看起来会像语言级别类型(如int)的零初始化一样。但实际上,这可能涉及到多个非平凡调用。如果构造函数被声明为内联函数且其实现是平凡的(零或常量初始化),编译器通常会理解这一点。 - Wolf
@Wolf 好的,我明白了。假设 map() 被平凡地初始化,那么我的代码就没问题了。哎呀,C++ 真是棘手 :) 谢谢你的解释。 - fen
1
我尝试了这种方法,但是出现了一个段错误。Register的调用在一个cpp文件中,而map是在另一个cpp文件中定义的。从调试器中可以看出,在“平凡”的初始化地图之前,对assign s_registered的调用发生了。我使用了Wolf下面的方法,其中有一个名为GetMap的函数。这确保静态映射在第一次使用时被创建。 - tree
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接