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

我们遇到了一些静态初始化顺序失败的问题 ,我正在寻找方法来梳理大量的代码,以查找可能的事件。 有关如何有效地做到这一点的任何build议?

编辑:我得到一些如何解决静态初始化顺序问题的好答案,但这不是我的问题。 我想知道如何find这个问题的对象。 埃文的答案在这方面似乎是最好的; 我不认为我们可以使用valgrind,但我们可能有内存分析工具,可以执行类似的function。 这只会在给定的构build中初始化顺序错误的情况下捕获问题,并且顺序可能随着每个构build而改变。 也许有一个静态分析工具可以解决这个问题。 我们的平台是在AIX上运行的IBM XLC / C ++编译器。

解决初始化的顺序:

首先,这只是一个暂时的解决方法,因为你有全局variables,你正试图摆脱,但没有时间(你将最终摆脱他们不是吗?:-)

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

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

multithreading问题:

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

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

但是,C ++ 03并没有正式保证静态函数对象的构造是线程安全的。 所以在技术上getInstance_XXX()方法必须用关键部分来保护。 好的一面是,gcc有一个明确的补丁作为编译器的一部分,它保证每个静态函数对象只有在有线程的情况下才会初始化一次。

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

创作问题:

在创build时,没有问题,因为我们保证在创build之前创build它。

销毁问题:

在对象被销毁之后存在一个访问对象的潜在问题。 这只会发生,如果你从另一个全局variables的析构函数访问对象(通过全球,我指的是任何非本地静态variables)。

解决办法是确保你强制销毁的顺序。
记住破坏的顺序与构造的顺序完全相反。 所以如果你访问析构函数中的对象,你必须保证对象没有被销毁。 为此,必须保证在构造调用对象之前完全构造对象。

 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. } }; 

我只是写了一些代码来追踪这个问题。 我们有一个很好的代码库(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 ENABLE-_-FIASCO-_-FINDER, 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 

并在解决scheme中的每个 .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文件中。 至less,这会给你一个设置断点的好地方,因为这段代码应该是绝对的第一个执行代码(在这之后,你可以遍历你的代码,看到所有正在初始化的全局variables) 。

笔记:

  • 将“FIASCO_FINDER”macros放在尽可能靠近文件顶部的位置非常重要。 如果你把它放在一些其他的#includes之下,你可能会在识别你所在的文件之前冒着崩溃的危险。

  • 如果您使用Visual Studio和预编译头文件,可以使用“查找并replace”对话框将所有#include“precompiledheader.h”replace为相同的文本加上FIASCO_FINDER行(如果您勾选正则expression式,则可以使用“\ n”插入多行replace文本)

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

然后进入每个函数来获取文件和函数名称(假设你已经编译了debugging信息)。 获得名称后,退出该函数(备份到_initterm )并继续,直到_initterm退出。

这给你所有的静态初始值设定项,而不仅仅是你的代码中的 – 这是获得一个详尽列表的最简单的方法。 你可以过滤出你无法控制的那些(比如第三方库中的那些)。

该理论适用于其他编译器,但函数的名称和debugging器的function可能会改变。

也许使用valgrind来查找未初始化内存的使用情况。 “静态初始化顺序失败”最好的解决scheme是使用一个静态函数,它返回对象的一个​​实例,如下所示:

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

这样你访问你的静态对象是通过调用getStatic,这将保证它在初次使用时被初始化。

如果您需要担心解除初始化的顺序,请返回一个新的对象而不是静态分配的对象。

编辑:删除多余的静态对象,我不知道为什么,但我混合和匹配两个方法有静态在我原来的例子。

有基本上“初始化”由编译器生成的C ++的代码。 find这个代码/调用堆栈的一个简单方法是创build一个静态对象,在构造函数中引用NULL,在debugging器中断开并稍微探索一下。 MSVC编译器设置一个迭代的静态初始化函数指针表。 你应该能够访问这个表格,并确定在你的程序中发生的所有静态初始化。

我们遇到了一些 静态初始化顺序失败的问题, 我正在寻找方法来梳理 大量的代码,以查找 可能的事件。 有关如何有效地做到这一点的 任何build议

这不是一个微不足道的问题,但如果你有一个易于parsing的中间格式的代码表示,至less它可以按照相当简单的步骤完成。

1)find所有具有不平凡构造函数的全局variables,并将其放入列表中。

2)对于每个非平凡构造的对象,生成由其构造函数调用的整个势函数树。

3)遍历非平凡构造函数树,如果代码引用了其他非平凡构造的全局variables(这些variables在第一步中生成的列表中非常有用),那么就有一个潜在的早期静态初始化顺序问题。

4)重复步骤2和3,直到用完第1步中生成的列表。

注意:如果你有一个类的多个全局variables的话,你可以通过每个对象类而不是每个全局实例访问一次potential-function-tree来优化。

将所有全局对象replace为全局函数,该全局函数返回对函数中声明为静态的对象的引用。 这不是线程安全的,所以如果你的应用程序是multithreading的,你可能需要一些像pthread_once或全局锁的技巧。 这将确保所有内容在使用前都被初始化。

现在,无论是你的程序工作(欢呼!),否则它坐在一个无限循环,因为你有一个循环依赖(需要重新devise),否则你继续下一个错误。

Gimpel Software(www.gimpel.com)声称,他们的PC-Lint / FlexeLint静态分析工具将检测到这样的问题。

我对自己的工具有过很好的经验,但是没有具体的问题,所以我不能保证他们能提供多less帮助。

你需要做的第一件事就是列出所有具有不平凡构造函数的静态对象。

鉴于此,您可能需要一次一个地插入它们,或者直接将它们全部replace为单例模式对象。

单身模式受到了很多批评,但懒惰的“按需”构build是解决现在和未来大部分问题的一个相当简单的方法。

旧…

 MyObject myObject 

新…

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

当然,如果你的应用程序是multithreading的,这可能会给你带来比第一个更多的问题。

其他答案是正确的,我只是想补充说,对象的getter应该在.cpp文件中实现,它不应该是静态的。 如果你在一个头文件中实现它,这个对象将会在你调用它的每个库/框架中创build。

如果你的项目是在Visual Studio中的(我已经用VC ++ Express 2005和Visual Studio 2008 Pro试了这个):

  1. 打开类视图(主菜单 – >视图 – >类视图)
  2. 展开解决scheme中的每个项目并单击“全局函数和variables”

这应该给你一个体面的清单所有的全球受到失败

最后,更好的方法是尝试从项目中删除这些对象(有时候说起来容易做起来难)。