【问题标题】:C++ global initialization order ignores dependencies?C++ 全局初始化顺序忽略依赖关系?
【发布时间】:2011-04-14 08:37:02
【问题描述】:

我认为我的问题最好用代码来描述:

#include <stdio.h>

struct Foo;

extern Foo globalFoo;

struct Foo {
    Foo() {
        printf("Foo::Foo()\n");
    }

    void add() {
        printf("Foo::add()\n");
    }

    static int addToGlobal() {
        printf("Foo::addToGlobal() START\n");

        globalFoo.add();

        printf("Foo::addToGlobal() END\n");

        return 0;
    }
};

Foo globalFoo;

int dummy = Foo::addToGlobal();

int main() {
    printf("main()\n");

    return 0;
}

以上打印(使用 gcc 4.4.3):

Foo::Foo()
Foo::addToGlobal() START
Foo::add()
Foo::addToGlobal() END
main()

这是我所期望的,并且看起来合乎逻辑。

但是,当我交换以下行时:

Foo globalFoo;
int dummy = Foo::addToGlobal();

进入这个:

int dummy = Foo::addToGlobal();
Foo globalFoo;

程序输出以下内容:

Foo::addToGlobal() START
Foo::add()
Foo::addToGlobal() END
Foo::Foo()
main()

似乎Foo 的实例方法正在使用尚未构造的实例调用!像在全局范围内移动变量声明这样简单的事情正在影响程序的行为,这让我相信(1)未定义全局变量的初始化顺序和(2)全局变量的初始化顺序忽略所有依赖项。它是否正确?是否可以确保在初始化dummy之前调用Foo的构造函数?

我要解决的问题是静态填充项目存储库(Foo 的静态实例)。在我当前的尝试中,我正在使用一个宏(除其他外)创建一个全局变量(在匿名命名空间中以避免名称冲突),其初始化会触发静态初始化。也许我从错误的角度解决了我的问题?有更好的选择吗?谢谢。

【问题讨论】:

    标签: c++


    【解决方案1】:

    (1)未定义全局变量的初始化顺序

    全局变量在单个翻译单元中(源文件)按照它们的定义顺序进行初始化。

    未指定不同翻译单元中全局变量的初始化顺序。

    (2) 全局变量的初始化顺序忽略所有依赖

    没错。

    是否可以确保 Foo 的构造函数在初始化 dummy 之前被调用?

    是的,如果globalFoo 定义在之前 dummy 并且它们在同一个翻译单元中。

    一种选择是有一个指向全局实例的静态指针;在进行任何动态初始化之前,此类指针将被初始化为 null; addToGlobal 然后可以测试指针是否为空;如果是,那么这是第一次使用全局,addToGlobal 可以创建全局 Foo

    【讨论】:

    • 通过“是否可以确保在初始化 dummy 之前调用 Foo 的构造函数?”我的意思是“除了改变定义的顺序”。你的回答让事情变得更清楚了,但它并没有真正“解决”我的问题(还)。
    • 在我看来,最好有一个 Foo 的静态成员,它是存储库。将 Foo 视为存储库(即 globalFoo)和其他目的并不“感觉正确”。另一方面,如果 Foo 有一个全局存储库,那么 init 应该可以正常工作,并且设计看起来更简洁。
    • 我宁愿将单例实现为静态函数:Foo&amp; global_foo() { static Foo foo; return foo; },因为在初始化顺序上有更好的保证:它将在第一次调用该方法时被初始化。在最终确定期间仍然会有问题......
    • @David - C++/CLI 的最终确定问题?
    【解决方案2】:

    按初始化顺序,阅读答案here

    关于如何解决初始化问题,您可以将全局推送为函数中的静态局部变量。有标准保证静态局部变量将在第一次调用函数时被初始化:

    class Foo {
    public:
       static Foo& singleton() {
          static Foo instance;
          return instance;
       }
    };
    

    然后您的其他全局变量将访问该变量:

    Foo::singleton().add();
    

    请注意,这通常被认为不是一个好的设计,而且即使这样解决了初始化问题,也没有解决终结的顺序,所以你应该注意不要在单例被销毁后访问它.

    【讨论】:

    • 我在发布我的问题后遇到了这个解决方案。我试过了,它似乎工作。但是,可能会出现一个问题:staticsingleton 内部会不会有多个实例?也就是说,如果 Foo::singleton() 在头文件中声明和定义并包含在各种翻译单元中,它是否可能返回不同的内容?
    • 这种模式的另一个潜在问题是 AFAIK 并发访问单例是不安全的。
    • @strager: 当局部变量被声明为static 时,它会有一个实例,即使函数是内联的(即在不同的翻译单元中单独编译),链接器必须确保只存在一个变量定义。
    • @FuleSnabel:在静态初始化(手头的问题)期间,应用程序仍然是单线程的。如果singleton 函数第一次由单个线程调用,则该模式与它封装的对象一样是线程安全的。如果第二个线程在第一个调用完成初始化之前调用该函数,则会出现线程安全问题。如果对象在单个线程中初始化,这是线程安全的。
    【解决方案3】:

    你说得对,翻译单元之间的全局变量初始化是未定义的。使用singleton pattern 可以解决此问题。但是,请注意,这种设计模式经常被滥用。另请注意,如果您在析构函数中有依赖关系,全局变量的顺序或销毁也是未定义的。

    【讨论】:

    • 销毁顺序其实是明确定义的:具有静态存储时长的对象按照初始化或构造完成时的相反顺序销毁(有一些细微差别,但这是一般规则) .
    【解决方案4】:

    为全局变量提供正确初始化顺序的最可靠方法...

    1) 初始化顺序取决于传递给链接器的目标文件顺序。直接或反向 - 没关系。您可以创建测试应用程序来检测它。

    2) 使用适当的实用程序(例如nm)来发现每个包含全局变量的目标文件的导入和导出。

    3) 构建依赖关系图,对目标文件进行排序并构建正确链接所需的顺序。如果存在,请手动解决循环。

    我在 Linux 上的 makefile 中使用了这样的过程。它有效...

    【讨论】:

      【解决方案5】:

      C++ 缺少像Ada's pragma elaborate 这样的东西,所以你不能对将发生的顺序初始化做出任何假设。抱歉。这很糟糕,但这就是设计。

      【讨论】:

        【解决方案6】:

        让静态全局变量成为一个初始化为 nullptr 的指针怎么样?然后在另一个全局对象尝试使用该对象之前,检查它的创建并在需要时创建。这对我创建类创建者的全局注册表很有用,可以在其中添加新类而无需更改处理注册表的文件。 即

        class Factory {
           static map<string, Creator*>* theTable;
           static void register1(const string& string, Creator* creator);
        ...
        };
        ...
        map<string, Creator*>* Factory::theTable= nullptr;
        void Factory::register1(const string& theName, Creator* creator) {   
            if (!theTable) theTable=new map<string, Creator*>;
            (*theTable)[theName]=creator;
        }
        

        这在 Visual Studio 2015 中编译并使用 VC++。

        在此之前我曾尝试过使用

        class Factory {
            public:
              static map<string, Creator*>  theTable;
              static map<string, Creator*>& getTable();
              static void register1(const string& string, Creator* creator);
        }
        map<string, Creator*>  Factory::theTable;
        map<string, Creator*>& Factory::getTable() {
           return theTable;
        }
        void Factory::register1(const string& theString, Creator* creator) {
           getTable()[theString]=creator; // fails if executed before theTable is created
        
        }
        

        但在尝试将条目插入映射之前未创建表时,我仍然会抛出异常,如果类的注册是在工厂逻辑的单独编译单元中处理的,则可能会发生这种情况。

        【讨论】:

          【解决方案7】:

          单个翻译单元(源文件)中的全局变量按照它们的定义顺序进行初始化。

          在此规则中添加注释很重要,即仅声明不定义顺序:

          extern Foo globalFoo; // or just a ref that is defined at a single place
          extern Foo & globalFooRef;
          

          或作为静态成员

          struct Global
          {
              static Foo globalFoo; // or just a ref that is defined at a single place
              static Foo & globalFooRef; 
          };
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-12-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-10-23
            • 2011-09-11
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多