寻找C++静态初始化顺序问题

71

我们遇到了静态初始化顺序惊异的一些问题,我正在寻找一种方法来检查大量代码中可能出现的情况。有没有关于如何高效地做到这一点的建议?

编辑:我得到了一些解决静态初始化顺序问题的好答案,但那不是我的问题。我想知道如何找到受此问题影响的对象。 Evan的回答似乎是目前为止在这方面最好的答案;我认为我们不能使用valgrind,但我们可能有能够执行类似功能的内存分析工具。那只会捕获初始化顺序对于给定构建错误的问题,而且顺序可以随每个构建而改变。也许有一个静态分析工具可以捕获这个问题。我们的平台是运行在AIX上的IBM XLC/C++编译器。


“Plus One”寻找它们的过程中我也遇到了同样的问题。几年前,我向GCC提交了一个功能请求,但是什么也没有发生。 - jww
相关链接:https://dev59.com/0F0a5IYBdhLWcg3w48Pn - YSC
12个回答

82

解决初始化顺序问题:

首先,这只是一个临时的解决办法,因为您有一些全局变量需要摆脱,但目前还没有时间(您最终会摆脱它们的,不是吗?:-)

class A
{
    public:
        // Get the global instance abc
        static A& getInstance_abc()  // return a reference
        {
            static A instance_abc;
            return instance_abc;
        }
};

这将确保在第一次使用时初始化它,并在应用程序终止时销毁它。

多线程问题:

C++11确保了这是线程安全的:

§6.7 [stmt.dcl] p4
如果控制同时进入声明并且正在初始化变量,则并发执行必须等待初始化完成。

但是,C++03没有正式保证静态函数对象的构造是线程安全的。因此,在技术上,getInstance_XXX() 方法必须受到关键部分的保护。好消息是,gcc作为编译器的一部分有一个显式补丁,即使存在线程也保证每个静态函数对象只会被初始化一次。

请注意:不要使用双重检查锁定模式试图避免锁定的成本。这在C++03中不起作用。

创建问题:

在创建时,没有问题,因为我们保证在可以使用之前就已经创建了它。

销毁问题:

销毁后访问对象可能存在潜在问题。只有当您从另一个全局变量(我指非局部静态变量)的析构函数中访问对象时才会发生这种情况。

解决方案是确保您强制销毁顺序。
请记住,销毁顺序与构造顺序完全相反。因此,如果您在析构函数中访问对象,则必须保证该对象未被销毁。为此,您必须仅确保在调用对象构造之前已完全构造了对象。

class B
{
    public:
        static B& getInstance_Bglob;
        {
            static B instance_Bglob;
            return instance_Bglob;;
        }

        ~B()
        {
             A::getInstance_abc().doSomthing();
             // The object abc is accessed from the destructor.
             // Potential problem.
             // You must guarantee that abc is destroyed after this object.
             // To guarantee this you must make sure it is constructed first.
             // To do this just access the object from the constructor.
        }

        B()
        {
            A::getInstance_abc();
            // abc is now fully constructed.
            // This means it was constructed before this object.
            // This means it will be destroyed after this object.
            // This means it is safe to use from the destructor.
        }
};

3
我不能摆脱它们。我没有创造它们。我只是被困在找到它们的任务中。据我所知,它们都是const对象,这并不太糟糕。 - Fred Larson
1
双重检查锁定在 C++ 中运行良好。虽然不能保证其运行,但实际上它确实可以正常工作。这两者之间有所不同。 - coryan
3
不仅涉及序列点,还包括CPU指令重新排序、推测执行以及跨多个CPU的缓存行失效等问题。使用线程API是确保正确性的唯一方式。 - Zan Lynx
11
@coryan:你忘了加上“直到它坏掉”的部分。 - peterchen
"C++03并没有正式保证静态函数对象的构建是线程安全的。实际上,它根本没有承认多线程的存在。" - Deduplicator
显示剩余18条评论

32

我刚刚写了一些代码来追踪这个问题。我们有一个相当大的代码库(1000+个文件),在Windows/VC++ 2005上运行良好,但在Solaris/gcc上启动时会崩溃。 我编写了以下的.h文件:

#ifndef FIASCO_H
#define FIASCO_H

/////////////////////////////////////////////////////////////////////////////////////////////////////
// [WS 2010-07-30] Detect the infamous "Static initialization order fiasco"
// email warrenstevens --> [initials]@[firstnamelastname].com 
// read --> http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 if you haven't suffered
// To enable this feature --> define E-N-A-B-L-E-_-F-I-A-S-C-O-_-F-I-N-D-E-R, rebuild, and run
#define ENABLE_FIASCO_FINDER
/////////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef ENABLE_FIASCO_FINDER

#include <iostream>
#include <fstream>

inline bool WriteFiasco(const std::string& fileName)
{
    static int counter = 0;
    ++counter;

    std::ofstream file;
    file.open("FiascoFinder.txt", std::ios::out | std::ios::app);
    file << "Starting to initialize file - number: [" << counter << "] filename: [" << fileName.c_str() << "]" << std::endl;
    file.flush();
    file.close();
    return true;
}

// [WS 2010-07-30] If you get a name collision on the following line, your usage is likely incorrect
#define FIASCO_FINDER static const bool g_psuedoUniqueName = WriteFiasco(__FILE__);

#else // ENABLE_FIASCO_FINDER
// do nothing
#define FIASCO_FINDER

#endif // ENABLE_FIASCO_FINDER

#endif //FIASCO_H

在解决方案中的每个.cpp文件内,我添加了以下内容:

#include "PreCompiledHeader.h" // (which #include's the above file)
FIASCO_FINDER
#include "RegularIncludeOne.h"
#include "RegularIncludeTwo.h"

当您运行应用程序时,您将获得一个输出文件,如下所示:
Starting to initialize file - number: [1] filename: [p:\\OneFile.cpp]
Starting to initialize file - number: [2] filename: [p:\\SecondFile.cpp]
Starting to initialize file - number: [3] filename: [p:\\ThirdFile.cpp]

如果你遇到了崩溃,罪魁祸首应该在最后一个列出的 .cpp 文件中。至少,这将为您提供一个很好的设置断点的地方,因为此代码应该是在您的代码执行之前(之后您可以逐步执行您的代码并查看正在初始化的所有全局变量)。
注意事项:
- 将 "FIASCO_FINDER" 宏放在文件顶部尽可能靠近的位置非常重要。如果将其放在某些其他 #包含语句下面,则有可能在确定所在文件之前就会崩溃。 - 如果您使用 Visual Studio 和预编译头,则可以使用“查找和替换”对话框将此额外的宏行添加到 所有 的 .cpp 文件中。将现有的 "#包含预编译头文件.h" 替换为相同的文本加上 FIASCO_FINDER 行(如果选中 "正则表达式",则可以使用 "\n" 插入多行替换文本)。

这个对于小项目似乎有帮助。可惜我有上千个实现文件和数十万行代码的代码库 :/ - Chad
Chad - 通常有一种快速替换的方法,即使在1000多个文件上也可以(例如,如果您已经有预编译头文件,您可以搜索/替换该头文件名称,或编写一个小脚本来替换所有文件)。我在我们的项目中采用了前者,而且有很多100个文件。 - Warren Stevens
2
@Chad 有了一个好的grep工具,比如grepWin,我们可以使用正则表达式来完成这个任务。将所有*.cpp/cxx/cc文件中的[\s\S]*(整个文件)替换为#include "HeaderContainingTheMacro.h"\nFIASCO_FINDER\n$0$0是为了在放置前言后将整个文件放回原处。 - Antonio

15

根据您使用的编译器,您可以在构造函数初始化代码处设置断点。在Visual C++中,这是_initterm函数,该函数给出要调用的函数列表的开始和结束指针。

然后进入每个函数以获取文件和函数名称(假设您已启用调试信息进行编译)。一旦您拥有了名称,就从函数中退出(返回到_initterm),并继续执行,直到_initterm退出。

这将提供给您所有静态初始化程序,而不仅仅是您代码中的初始化程序 - 这是获取详尽列表的最简单方法。您可以过滤掉那些您无法控制的初始化程序(例如第三方库中的初始化程序)。

其他编译器也适用这一理论,但函数名称和调试器的功能可能会有所改变。


5

编译器生成的代码基本上是“初始化”C++的代码。要找到这个代码/调用堆栈的简单方法是在构造函数中创建一个静态对象,其中包含对NULL进行解引用的内容 - 在调试器中断点并探索一下。MSVC编译器设置了一个函数指针表,可以迭代静态初始化。您应该能够访问此表并确定程序中发生的所有静态初始化。


5

也许可以使用Valgrind来查找未初始化内存的使用情况。解决“静态初始化顺序混乱”的最好方法是使用一个静态函数,该函数返回一个对象实例,就像这样:

class A {
public:
    static X &getStatic() { static X my_static; return my_static; }
};

通过调用getStatic方法来访问静态对象,这将确保在首次使用时进行初始化。
如果你需要担心反初始化的顺序,返回一个新的对象而不是一个静态分配的对象。
编辑:删除了冗余的静态对象,我不知道为什么,在我的原始示例中混合并匹配了两种静态方式。

这个答案很好。并且反初始化也是有保证的:它是构造函数完成的逆过程,还可以跨翻译单元。所以我认为一切都很好。它甚至不会泄漏。 - Johannes Schaub - litb
@litb,C++ FAQ Lite 在析构顺序方面似乎与您不同意。请参见http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12。您对标准很了解——有没有什么证据证明 Marshall Cline 是错的?8v) - Fred Larson
1
我认为使用静态对象引用的问题在于,在程序终止期间,您可能会得到对已死对象的引用。 - Evan Teran
@Evan,我觉得你的例子有些不对。getStatic()方法返回一个值,但似乎与something_static成员没有任何关系。something_static有什么作用吗? - Fred Larson
糟糕!我在打字时想到了两件不同的事情,现在已经修复了。 - Evan Teran
显示剩余3条评论

4
我们遇到了静态初始化顺序混乱的问题,并正在寻找方法来遍历大量代码以查找可能的出现情况。如果您拥有易于解析的中间格式表示代码,则至少可以按照相当简单的步骤完成此操作。
1)查找所有具有非平凡构造函数的全局变量,并将它们放入列表中。
2)对于这些非平凡构造对象中的每一个,生成整个由它们的构造函数调用的潜在函数树。
3)遍历非平凡构造函数树,如果代码引用任何其他非平凡构造的全局变量(这些变量很方便地在步骤一中生成的列表中),则存在潜在的早期静态初始化顺序问题。
4)重复执行第2和第3步,直到耗尽步骤一中生成的列表为止。
注意:如果您有单个类的多个全局变量,则可以通过仅访问每个对象类的潜在函数树一次而不是每个全局实例一次来优化此过程。

1

将所有全局对象替换为返回在函数中声明为静态的对象引用的全局函数。这不是线程安全的,因此如果您的应用程序是多线程的,您可能需要一些技巧,如pthread_once或全局锁。这将确保在使用之前初始化所有内容。

现在,要么您的程序可以正常工作(万岁!),要么它会陷入无限循环,因为您有一个循环依赖关系(需要重新设计),要么您继续解决下一个错误。


1
Gimpel Software(www.gimpel.com)声称他们的PC-Lint/FlexeLint静态分析工具可以检测出这些问题。
我使用过他们的工具,效果不错,但对于这个特定问题我无法保证它们能提供多少帮助。

1

首先,您需要列出所有具有非平凡构造函数的静态对象列表。

在此基础上,您可以逐个处理它们,或者仅将它们全部替换为单例模式对象。

单例模式经常受到批评,但是懒惰的“按需”构建是一种相当简单的方法,可以解决现在和未来大部分问题。

旧的...

MyObject myObject

新的...

MyObject &myObject()
{
  static MyObject myActualObject;
  return myActualObject;
}

当然,如果你的应用程序是多线程的,这可能会给你带来比一开始更多的问题...


1
这不是单例模式。它有类似的实现,但并不是同一件事情。 - Martin York
1
Scott Meyers 告诉我是这样的;-) [《Effective C++,第二版》,第222页] - Roddy
1
我称上述模式为“首次使用时构建”模式。仅为澄清起见,单例模式确保一个类只有一个实例,并提供对它的全局访问点。'首次使用时构建'是/可以成为单例实现中提供'全局访问点'部分的一部分。但是,仅靠它自己不足以确保单个实例。 - Chris Bednarski
这如何解决问题?这是否提供了定义的初始化/反初始化顺序? - paulm
@paulm - “首次使用时构建”意味着在构建对象之前无法使用它(典型问题是两个全局对象在不同的编译单元中)。它们按相反的顺序被销毁,因此是有定义的顺序。 - Roddy

1

这些回答中有一些已经过时了。为了方便像我这样从搜索引擎来的人:在Linux和其他地方,可以通过Google的AddressSanitizer找到这个问题的实例。

AddressSanitizer是LLVM的一部分,从版本3.1开始,并且是GCC的一部分,从版本4.8开始

然后您可以执行以下操作:

$ g++ -fsanitize=address -g staticA.C staticB.C staticC.C -o static 
$ ASAN_OPTIONS=check_initialization_order=true:strict_init_order=true ./static 
=================================================================
==32208==ERROR: AddressSanitizer: initialization-order-fiasco on address ... at ...
    #0 0x400f96 in firstClass::getValue() staticC.C:13
    #1 0x400de1 in secondClass::secondClass() staticB.C:7
    ...

更多细节请参见此处: https://github.com/google/sanitizers/wiki/AddressSanitizerInitializationOrderFiasco


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