我读到了C ++中与应用程序崩溃有关的静态初始化顺序fiasco。我想我理解但仍然没有问题:
1)如果我想重现这个问题,我该怎么办(这样我的程序应该崩溃)?我想编写一个测试程序来重现崩溃。如果可能的话,能否提供源代码?
2)我读了这篇C++ FAQ Lite文章,它说它有两个静态对象,x和y,在两个不同的文件中,y调用x的方法。作为全局静态成员具有文件级别范围的可能性如何?
3)这个问题非常危险,有没有尝试在编译器级别修复它?
4)C ++专家在现实生产中遇到过这个问题多少次?
答案 0 :(得分: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 :(得分:2)
1)您必须检查运行时启动代码,以了解它如何选择初始化顺序或稍微进行实验。您可以通过在两个以上的对象(可能是3或4个)之间创建初始化依赖关系来提高出错的几率。
2)只需在文件级别实例化一个对象:
OBJECT_TYPE x;
3)没有编译器解决我所知道的问题。它需要在链接时或之后进行检测。
4)在实践中,很容易避免:使所有初始化都是自包含的。
答案 2 :(得分:1)
“1)如果我想重现这个问题,我怎么能这样做(这样我的程序应该崩溃)?我想编写一个测试程序来重现崩溃。如果可能的话,请你提供源代码?“
您无法编写便携式测试用例。静态初始化顺序fiasco是未定义的顺序。如果有人编写的代码如果在一个合法的订单中初始化,则会出现问题,但如果他们在其他合法的订单中初始化则会失败。因此,出于同样的原因,您不能保证它会起作用,您无法保证它会失败。这就是重点。
您可能会猜测链接器将在所有全局变量从另一个变换单元之前初始化所有全局变量。因此设置两个源文件A和B,A中的全局A1和A2,B中的B1和B2。然后在构造函数中使用B1(我的意思是“执行某些操作失败,如果B1尚未初始化”) A1的结构,并在B2的构造函数中使用A2。也在A2的构造函数中使用A1(并在A中按顺序声明它们)。然后唯一不会失败的顺序是B1,A1,A2,B2,您可能会想到这是一个非常不可能的实现选择。在一个特定的实现中,如果它确实以某种方式成功,切换事物使A2使用B2而不是使用A2的B2,并且只希望不会改变初始化顺序。
当然你也可以在B1的构造函数中使用B2(并在B中按顺序声明它们),以保证无论初始化顺序如何都失败。但那不会是静态初始化顺序惨败,它只是一个根本上破坏的循环依赖。
“2)我读了这篇C ++ FAQ Lite文章,它说它有两个静态对象,x和y,在两个不同的文件中,y调用x的方法。全局静态成员有可能具有文件级范围吗?”
例如,在两个翻译单元中声明它们extern
(可能使用公共标题)。范围,联系和存储持续时间都是不同的。
“3)这个问题非常危险,有没有尝试在编译器级别修复它?”
不是我知道的。我很确定这是一个停顿问题,弄清楚对象X是否在其构造函数中“使用”(在我上面定义的意义上)对象Y,因此在链接时构造依赖图并对其进行t排序将是最好的部分措施。
“4)C ++专家在现实生产中遇到过这个问题的次数是多少次?”
永远不会,因为(a)我不会让全局变形,并且(b)我使用过它们,我避免在初始化器中做任何奇特的事情。基本上,不要设计一个类然后决定拥有它的全局实例 - 如果你打算使用全局对象,那么将它设计为全局对象。并尽可能使用本地范围的静态而不是全局静态。如果你需要提供看起来像全局的东西,可以将它作为一个函数发布,它返回对象的引用,或者作为一个对象,它们可以在它们的堆栈上创建,并为它们调用该函数,然后充当代理(或处理,如果你愿意)为全球国家。你仍然需要担心线程安全,但是线程环境提供了管理它的方法,而除了让你的调用者在他们的翻译单元中定义你的全局变量之外,没有办法管理惨败。
如果您实现定义全局变量的API(例如std::out
),那就很难了。您可以使用一个技巧,在同一个标头中定义虚拟文件范围变量,声明全局变量。但是我不记得这个名字了。