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

Fre*_*son 59 c++ initialization static-order-fiasco

我们遇到了静态初始化命令惨败的一些问题,我正在寻找方法来梳理大量代码以找到可能发生的事件.有关如何有效地做到这一点的任何建议?

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

Mar*_*ork 66

解决初始化顺序:

首先,这只是一个临时的解决方法,因为你有全局变量,你试图摆脱,但还没有时间(你最终会摆脱它们不是吗?:-)

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

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

多线程问题:

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.
        }
};
Run Code Online (Sandbox Code Playgroud)

  • @coryan:你忘记添加"..中断它". (10认同)
  • @coryan,@ Martin York:不仅是序列点,而是CPU指令重新排序,推测执行和跨多个CPU的缓存行无效等.使用线程API,这是唯一确定的方法. (3认同)
  • 我无法摆脱它们.我没有创造它们.我只是坚持找到它们的任务.据我所知,它们都是const对象,并不是那么糟糕. (2认同)

War*_*ens 31

我刚写了一些代码来追踪这个问题.我们有一个很好的大小代码库(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
Run Code Online (Sandbox Code Playgroud)

并在每个溶液中的.cpp文件,我加了这一点:

#include "PreCompiledHeader.h" // (which #include's the above file)
FIASCO_FINDER
#include "RegularIncludeOne.h"
#include "RegularIncludeTwo.h"
Run Code Online (Sandbox Code Playgroud)

运行应用程序时,您将获得如下输出文件:

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]
Run Code Online (Sandbox Code Playgroud)

如果遇到崩溃,罪魁祸首应该在列出的最后一个.cpp文件中.至少,这将为您提供设置断点的好地方,因为此代码应该是您的代码的绝对首要执行(之后您可以单步执行代码并查看正在初始化的所有全局变量) .

笔记:

  • 将"FIASCO_FINDER"宏尽可能靠近文件顶部非常重要.如果你把它放在其他一些#includes之下,你就会在识别你所在的文件之前冒着崩溃的风险.

  • 如果您正在使用Visual Studio和预编译的标题,则可以使用"查找和替换"对话框快速完成将此额外宏行添加到所有 .cpp文件中,以替换现有的#include"precompiledheader.h"相同的文本加上FIASCO_FINDER行(如果你勾选"正则表达式,你可以使用"\n"插入多行替换文本)

  • @Chad 有一个很好的 grep 工具,比如 [grepWin](https://sourceforge.net/projects/grepwin/),你可以用正则表达式来做到这一点。对于所有 *.cpp/cxx/cc 文件,将 `[\s\S]*`(整个文件)替换为 `#include "HeaderContainingTheMacro.h"\nFIASCO_FINDER\n$0`。`$0` 是在放置序言后放回整个文件。 (2认同)

pax*_*blo 14

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

然后单步执行每个函数以获取文件和函数名称(假设您已使用调试信息编译).获得名称后,退出该功能(备份至_initterm)并继续直至_initterm退出.

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

该理论适用于其他编译器,但函数名称和调试器的功能可能会发生变化.


Eva*_*ran 5

也许使用valgrind来找到未初始化内存的用法."静态初始化顺序fiasco"最好的解决方案是使用一个静态函数,它返回一个对象的实例,如下所示:

class A {
public:
    static X &getStatic() { static X my_static; return my_static; }
};
Run Code Online (Sandbox Code Playgroud)

这样您通过调用getStatic来访问静态对象,这将保证在首次使用时初始化它.

如果您需要担心去初始化的顺序,请返回一个new'd对象而不是静态分配的对象.

编辑:删除冗余的静态对象,我不知道为什么,但我混合和匹配两个方法在我的原始示例中一起静态.


小智 5

有一些代码基本上"初始化"由编译器生成的C++.当时查找此代码/调用堆栈的一种简单方法是创建一个静态对象,其中包含在构造函数中取消引用NULL的内容 - 在调试器中中断并探索一下.MSVC编译器设置一个函数指针表,迭代用于静态初始化.您应该能够访问此表并确定程序中发生的所有静态初始化.