我读到了 C++ 中与应用程序崩溃相关的静态初始化顺序惨败。我想我明白了,但仍然有几个问题:
1)如果我想重现这个问题,我该怎么做(这样我的程序就会崩溃)?我想编写一个测试程序来重现崩溃。如果可以的话,能否提供一下源代码?
2)我阅读了这篇C++ FAQ Lite文章,它说它在两个不同的文件中有两个静态对象 x 和 y,并且 y 调用 x 的方法。作为全局静态成员,它如何具有文件级范围?
3)这个问题非常危险,有没有尝试在编译器级别修复它?
4) C++ 专家在实际生产中遇到过多少次这个问题?
3 回答
编辑:根据评论进行调整以使其更准确。
一个很好的例子如下所示:
// A.cpp
#include "A.h"
std::map<int, int> my_map;
// A.h
#include <map>
extern std::map<int, int> my_map;
// B.cpp
#include "A.h"
class T {
public:
T() { my_map.insert(std::make_pair(0, 0)); }
};
T t;
int main() {
}
问题是实例t
可能在my_map
对象之前构造。因此插入可能发生在尚未构建的对象上。导致崩溃。
一个简单的解决方案是改为执行以下操作:
// A.h
#include <map>
std::map<int, int> &my_map()
// A.cpp
#include "A.h"
std::map<int, int> &my_map() {
// initialized on first use
static std::map<int, int> x;
return x;
}
// B.cpp
#include "A.h"
class T {
public:
T() { my_map().insert(std::make_pair(0, 0)); }
};
T t;
int main() {
}
通过函数访问静态对象,我们可以保证初始化的顺序,因为函数作用域的静态变量在第一次使用时就被初始化了。因此,t
首先构造对象,它调用my_map()
它在第一次运行时创建一个静态地图对象,然后返回对它的引用。
1)您要么必须检查运行时启动代码以查看它如何选择初始化顺序,要么进行一些实验。您可以通过在两个以上的对象(可能是 3 个或 4 个)之间创建初始化依赖关系来提高出现错误的几率。
2)只需在文件级别实例化一个对象:
OBJECT_TYPE x;
3)据我所知,没有编译器解决这个问题。它需要在链接时或链接后进行检测。
4)在实践中,很容易避免:使所有初始化自包含。
“1)如果我想重现这个问题,我该怎么做(这样我的程序应该崩溃)?我想编写一个测试程序来重现崩溃。如果可能,您能否提供源代码?
您不能编写可移植的测试用例。静态初始化订单惨败是订单未定义。当有人编写的代码在以一种合法顺序初始化时可以工作,但如果以其他合法顺序初始化时会失败,就会出现问题。因此,出于同样的原因,你不能保证它会起作用,你也不能保证它会失败。这就是重点。
您可能会猜测链接器将在另一个翻译单元的所有全局变量之前初始化来自一个翻译单元的所有全局变量。所以设置两个源文件A和B,A中的全局变量A1和A2,B中的B1和B2。然后在构造函数中使用B1(我的意思是“如果B1没有被初始化,做一些失败的事情”) A1,并在 B2 的构造函数中使用 A2。还要在 A2 的构造函数中使用 A1(并在 A 中按该顺序声明它们)。那么唯一不会失败的顺序是 B1、A1、A2、B2,您可能会认为这对于实现来说是一个不太可能的选择。在特定的实现中,如果它确实成功了,请切换以使 A2 使用 B2 而不是 B2 使用 A2,并且只希望这不会改变初始化顺序。
当然你也可以在 B1 的构造函数中使用 B2(并在 B 中按顺序声明它们),以保证无论初始化顺序如何都会失败。但这不会是静态初始化顺序的失败,它只是一个从根本上破坏的循环依赖。
“2)我阅读了这篇 C++ FAQ Lite 文章,它说它在两个不同的文件中有两个静态对象 x 和 y,并且 y 调用 x 的方法。作为全局静态成员,它如何具有文件级范围?
例如,extern
在两个翻译单元中声明它们(可能使用公共标头)。范围、链接和存储时间都是不同的东西。
“3)这个问题非常危险,有没有尝试在编译器级别修复它?”
从来没听说过。我很确定在其构造函数中确定对象 X 是否“使用”(在我上面定义的意义上)对象 Y 是一个暂停问题,因此在链接时构建依赖关系图并对其进行 t 排序最好是部分措施。
"4) 你们 C++ 专家在实际生产中遇到过多少次这个问题?
从来没有,因为 (a) 我不会把全局变量放在周围,并且 (b) 在我使用它们的地方,我避免在它们的初始化程序中做任何花哨的事情。基本上,不要设计一个类,然后决定拥有它的全局实例——如果你要使用全局对象,请将其设计为全局对象。尽可能使用局部范围的静态而不是全局静态。如果您需要提供看起来像全局的东西,请将其发布为返回对对象的引用的函数,或者作为他们可以在堆栈上创建的对象,并为他们调用该函数,然后充当代理(或句柄,如果你喜欢的话)用于全局状态。你仍然需要担心线程安全,但是线程环境提供了管理它的方法,
只有在实现定义全局变量的 API 时才会变得困难,例如std::out
. 您可以使用一个技巧,您可以在声明全局的同一标头中定义一个虚拟文件范围变量。不过我不记得名字了。