C++11中的延迟初始化顺序

3
请问需要翻译成哪种语言呢?
struct A
{
    void Register(const char* s);

    const char* m_s[10];
    int m_i = 0;
};

A& GetA();

a.cpp:

#include "a.h"
#include <stdio.h>

void A::Register(const char* s)
{
    m_s[m_i++] = s;
}

A& GetA()
{
    static A instance;
    return instance;
}

int main(int argc, char* argv[])
{
    A& a = GetA();
    int n = a.m_i;
    for (int i = 0; i < n ; ++i)
        printf("%s\n", a.m_s[i]);
    return 0;
}

b.cpp:

#include "a.h"
struct B
{
    B() { GetA().Register("b"); }

    static B* instance;
};
B* B::instance = new B;

c.cpp:

#include "a.h"
struct C
{
    C() { GetA().Register("c"); }

    static C* instance;
};
C* C::instance = new C;

代码使用 gcc (-std=c++11) 编译和运行良好,输出如下:
c
b

现在,参考cppreference.com

延迟动态初始化

对于动态初始化是否在主函数的第一个语句(静态成员)或线程的初始函数(线程本地)之前发生是由实现定义的,还是推迟发生。

如果非内联变量的初始化被推迟到main/thread函数的第一个语句之后才发生,那么它会在与要初始化的变量在同一翻译单元中定义的任何具有静态/线程存储期的变量的第一个odr-use之前发生。如果从给定的翻译单元中没有使用odr-used变量或函数,则该翻译单元中定义的非本地变量可能永远不会被初始化(这模拟了按需动态库的行为)。然而,只要来自TU的任何内容都被odr-used,所有具有副作用的初始化或销毁的非本地变量都将被初始化,即使它们没有在程序中使用。

请注意,a.cpp不知道BC的存在,而BCA的唯一交互是其各自实例的构造期间对GetA()A::Register()的调用。
就我所看到的,BC实例都不是ODR-used,当然也不是从main()的翻译单元中使用。它们的初始化显然具有副作用,但在main()进入之前或在main()打印注册字符串之前,甚至根本没有保证此初始化将发生。
因此,我的问题是: 是否由于gcc的实现定义行为而导致BC实例在main()打印已注册字符串之前进行初始化? 如果是由标准保证的,那么是如何保证的?

动态初始化的问题在于它们在不同的翻译单元之间的相对顺序是未定义的。但是大多数编译器都支持__attribute__((init_priority(x))) - Passer By
@故事讲述者:有什么保证,如果它们被初始化,那只会在 main 函数执行之前吗? - Jeremy
显然什么也没有。我读错了章节。我想到了静态初始化,并将其应用于动态初始化。在main函数中唯一的保证是B::instance在main开始执行之前为nullptr。 - StoryTeller - Unslander Monica
这使得你的问题非常有趣,考虑到所有的实现定义行为都存在 :) - StoryTeller - Unslander Monica
在考虑动态初始化的执行点时,还需要考虑本段落中的注释:“应该选择这些点以使程序员能够避免死锁”(http://eel.is/c++draft/basic.start.dynamic#4)。我认为这适用于更广泛的情况。如果`A`在主函数中被odr使用,编译器不能随意注入`B`和`C`的初始化,因为这可能会干扰`A`。因此,它必须在主函数开始执行之前或之后完全排序。 - StoryTeller - Unslander Monica
该部分的注释36也很有趣(类似措辞出现在C++11标准中),但它仍然没有澄清初始化发生的时间。 - Jeremy
1个回答

0

main() 打印注册字符串之前,BC 实例被初始化的事实是由于 gcc 的实现定义行为而不是标准吗?

这并不是标准保证的。引用中最相关的部分:

如果从给定的翻译单元中没有使用 odr 使用变量或函数,则该翻译单元中定义的非本地变量可能永远不会被初始化。

由于从 b.cppc.cpp 中没有使用 odr 使用任何函数或变量,因此它们的静态变量可能未初始化(关于动态初始化),因此它们的初始化副作用可能不可见。


实际上,当翻译单元被静态链接时,我会期望显示初始化行为,并且当它们被动态加载(共享库)时可能存在非初始化行为。但是标准并不保证这两种情况,因为它没有指定共享库的行为方式。

C++11标准规定:“具有副作用初始化的静态存储期非局部变量,即使没有odr使用,也必须进行初始化。”(注34)。但它仍然没有澄清这个初始化相对于main()中任何语句发生的时间。 - Jeremy
@Jeremy 的注释并不是规范性的。它似乎与给出一个例外情况,即变量/函数未被 odr-used 的 TU 的规则相冲突:“然而,只要来自 TU 的任何内容都被 odr-used,所有具有副作用的非本地变量的初始化或销毁都将被初始化,即使它们在程序中没有被使用。”当规则与注释相冲突时,规则优先,注释无效。 - eerorika
@Jeremy,我的草稿副本(比C++11晚得多)的注释如下:“即使没有odr使用,具有静态存储期和带有副作用初始化的非局部变量在这种情况下也会被初始化”,其中我认为“在这种情况下”指的是“在同一翻译单元中定义的任何非内联函数或非内联变量的任何非初始化odr使用之前……”。因此,这个冲突似乎已经解决了。 - eerorika
是的,我同意在C++11之后,措辞上的冲突似乎已经得到解决 - 这让我们得出结论:“程序中哪些线程以及在哪些点上进行延迟动态初始化是由实现定义的。”因此理论上,一种反常的编译器可以选择在退出时执行所有这样的初始化。 - Jeremy
@Jeremy 没错。这个例子依赖于编译器来初始化静态变量,因为标准并不保证它会被初始化。 - eerorika
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接