再次讨论C ++中的静态初始化顺序

时间:2013-12-11 10:49:45

标签: c++ initialization

我有一个代码:

A.H

...
namespace X
{
    const std::string Foo = "foo";
    inline std::string getFoo()
    {
        return Foo;
    }
}
...

a.cpp:

#include "a.h"
...
namespace X
{
   const string Default_Foo = getFoo();
}
...

当然,项目中有更多文件包含a.h

该程序在开始时导致段错误。调查显示:

  1. 在程序中创建了几个Foo副本
  2.     bash-4.2# nm -oC a.out | grep Foo
        a.out:3c162c50 b X::Foo
        a.out:3c162990 b X::Foo
        a.out:3c1641b0 b X::Foo
    

    2.在初始化期间,Default_Foo调用getFoo(),它不从a.cpp编译单元获取已经初始化的Foo,而是从另一个编译单元获取Foo,这很可能尚未初始化。这显然会导致段错误。

    有人可以给我这样的行为推理吗? 未来针对此类问题的最佳防御策略是什么?

    最后我最感兴趣的是为什么getFoo()使用来自另一个编译单元的Foo。

3 个答案:

答案 0 :(得分:3)

您的程序违反了一个定义规则(ODR)。

内联函数可以在多个编译单元中定义,但要求必须一致地定义它们,也就是说,每个定义都由相同的词汇序列组成,并且该序列中的每个使用的标识符必须表示相同的对象。

在您的情况下,标识符Foo表示编译单元之间的不同对象(默认情况下,修饰符const的名称空间级对象具有内部链接,因此每个编译单元都有自己的{{1} })。

用于构建程序的工具可能(但不是必须)诊断违反此规范的行为。在你的情况下,他们没有。链接器刚刚选择了X::Foo的第一个定义而没有仔细考虑它。

答案 1 :(得分:2)

创建了X::Foo的多个副本,因为您在标头中声明了定义 X::Foo。因此,当您将X::Foo包含在源中时,您将获得尽可能多的声明和定义。实际上,由于多个定义,它通常会在链接时导致错误,但是通过X::Foo内部链接可以避免这种错误。 (请参阅 legends2k 评论以获得解释)

静态初始化本身只在编译单元内有一个顺序。在多个编译对象之间获得适当的静态初始化顺序确实是一个问题。有特定于编译器的扩展来实现这一点,但请注意它们有严重的限制。

提供的代码方式我看不出为什么X::getFoo()调用不会被内联,从而消除了动态符号解析的任何机会。 X::Foo本身也不应该被名字弄乱。您应检查生成二进制文件的符号名称/重定位表,以查看是否动态解析了X::getFoo()X::Foo

避免此问题的解决方案之一是避免全局静态变量。相反,使用这种方法:

static const std::string& getFoo()
{
    static const std::string val = "abc";
    return val;
}

您将获得以下承诺:

  • val将在首次调用getFoo()
  • 时初始化
  • 是线程安全的

答案 2 :(得分:0)

在另一个论坛讨论这个问题之后,我倾向于认为问题的根源在于一个事实,即编译器为每个翻译单元创建多个getFoo()副本,每个翻译单元从该单元引用Foo。在链接期间,链接器认为所有getFoo()都是等效的并且拾取第一个,这不能保证是来自a.cpp转换单元的那个。

欢迎任何评论。