【问题标题】:Order of initialization初始化顺序
【发布时间】:2012-01-03 06:08:00
【问题描述】:

我的问题与全局对象的初始化顺序有关。这里有一定程度的讨论:C++: When (and how) are C++ Global Static Constructors Called?

我想知道的是,如何保证某些系统库对象(虽然想不出一个例子)在应用程序中使用之前被初始化? (我知道解决方案是将此类对象包装在一个函数中,但我想了解它在今天的编译器中是如何处理的)

这在 John Levine 的“链接器和加载器”一书中也有讨论,同时谈到 .init 和 .finit 部分,但目前的技术状态尚不清楚。

【问题讨论】:

    标签: c++ initialization


    【解决方案1】:

    一个例子是cout

    编译器并不能真正保证这些对象在使用之前就已构建。拿这个代码:

    struct Foo
    {
    
        Foo();
    }foo;
    
    #include <iostream>
    
    Foo::Foo()
    {
        std::cout << "Hello World";
    }
    
    int main() {}
    

    因为 foo 对象是在 cout 之前构建的,所以会出现段错误。当它尝试使用 cout 时,会发生不好的事情。

    基本上,所有全局对象都会插入代码来构造它们,这些代码在 main 之前运行。一旦你在 main 中,你就安全了,所有的对象都被构建了。在此之前,它取决于定义它们的顺序。这就是为什么我可以通过在声明 foo 全局后包含 iostream 来打破它。

    【讨论】:

    • 动态初始化可以在 main 启动之后发生,只要是在第一次使用之前。
    • cout 不是在不同的翻译单元中定义的吗?那么,即使在定义 Foo 之前包含了 iostream,是不是不能保证顺序?
    • @Pubby,不知道,有参考吗?
    • @Chethan,不知道标准对此有何规定。我的实现在iostream 中有一个静态的 __ioinit 对象,我认为它负责。
    • @Chethan:这很重要。编译器有技巧来处理初始化和 iostreams 的问题。一种常见的模式是拥有一个私有类型,该类型计算创建的类型实例的数量,并在私有名称空间内的iostream 中定义该类型的名称空间级别变量。当这些对象的构造函数作为 TU 的静态初始化的一部分运行时,它会检查它是否是第一个对象,在这种情况下它会初始化 iostream。因为这些对象位于包含iostream 的所有翻译单元中,所以保证了执行顺序
    【解决方案2】:

    可以使用编译器属性:

    在标准 C++ 中,保证在命名空间范围内定义的对象的初始化顺序严格按照它们在给定翻译单元中的定义顺序进行。不保证跨翻译单元的初始化。但是,GNU C++ 允许用户通过指定相对优先级来控制在命名空间范围内定义的对象的初始化顺序 init_priority 属性,这是一个当前介于 101 和 65535 之间的常量整数表达式。数字越小表示优先级越高。

    Some_Class  A  __attribute__ ((init_priority (2000)));
    Some_Class  B  __attribute__ ((init_priority (543)));
    

    我认为 101 以下是为实现而保留的,例如cout

    http://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes

    【讨论】:

    【解决方案3】:

    有几种方法可以确保全局变量在使用前被初始化。但是,这些往往是特定于平台的,并且系统之间的细节有所不同。应该在任何用户代码(包括全局变量初始化)运行之前初始化的全局对象的典型示例是全局流对象,特别是 std::cout。我使用了三种方法来确保提前初始化 std::cout:

    1. 标准库已经定义了std::ios_base::Init,它计算了它的构建频率。通过在标头&lt;iostream&gt; 中添加static std::ios_base::Init _Init;,可以在计数器从0 更改为1 时使用放置new 初始化全局流对象,并在销毁期间计数器从1 更改为0 时显式销毁。为了避免双重初始化,std::cout 的定义将只保留必要的空间,例如使用char std::cout[sizeof(_Standard_output_stream)];。如果用户代码在包含 &lt;iostream&gt; 之前定义了对象,则此方法可能会中断。
    2. 加载共享库时,在共享库中的对象可用之前运行一些初始化代码(卸载共享库时的一些破坏代码也是如此)。通过将相应的对象放入共享库中,可以在访问对象之前运行初始化代码。主要缺点是这需要使用共享库,这并不总是可取的。
    3. 链接器正在有效地构建初始化列表。在某些系统上,可以保证构建各种目标文件的顺序与看到的目标文件的顺序相反。也就是说,放置例如std::cout 进入最后一个 [C++] 库中的最后一个对象文件会导致该对象在其他对象之前被初始化。由于用户可以控制链接线,因此这不一定有效。

    此列表中最安全的方法是将对象放入共享库中。最好的方法可能是通过函数访问全局对象来避免该问题。如果线程可以在构造发生之前启动,那么所有方法都需要一些额外的工作。此外,我确信有特定于平台的方法可以解决这个问题。一般的观察是全局对象是邪恶的:尽量不要使用它们!

    【讨论】:

    • 我检查了 iostream 标头,1. 似乎是这样。我很好奇如何避免/处理双重初始化。能详细说明一下吗?
    • 如果我理解正确,您建议在动态库中使用cout,但这不会解决问题。相反,如果生成的二进制文件首先加载包含cout 的程序/库,然后再加载其余部分(这将确保加载时的初始化顺序),则将 其他所有内容 移动到动态库可能会.但缺点是所有带有静态变量的东西都必须在动态库中,这是您无法负担的奢侈品。
    • 在 3 上,问题(不是真正的问题)是这需要标准库实现和工具链之间的紧密耦合。特别是您需要将特定的 C++ 知识注入链接器,我认为现在不需要。链接器必须检测程序中cout 的存在并将初始化移动到最后一个对象(或任何合适的位置),以便首先对其进行初始化。现在,如果你还编译了一个使用 cout 的 dll,你将再次遇到类似的问题:加载程序必须检测...
    • ... 主程序在加载动态库时是否已经初始化cout,这意味着您再次将语言知识注入加载程序,而不是拥有语言不可知的加载器,只需要找到一个入口点并开始执行它。
    猜你喜欢
    • 2010-09-17
    • 1970-01-01
    • 2011-10-20
    • 1970-01-01
    • 1970-01-01
    • 2012-02-16
    • 2021-07-12
    • 2011-03-06
    相关资源
    最近更新 更多