静态初始化顺序惨败是否适用于 C?

Is static initialization order fiasco applicable to C?

提问人:blonded04 提问时间:11/16/2023 最后编辑:Lundinblonded04 更新时间:11/17/2023 访问量:175

问:

我在互联网上找到的关于静态初始化顺序惨败的所有内容都是关于 C++ 的,但是如果我初始化某种类型的全局变量,这是真的吗 Foo 像

struct Foo {
    int flag;
    pthread_key_t key;
    void *ptrs[10];
};

我无法初始化类型的变量,如?如果我想因为 SIOF 而获得正确的代码?struct Foostatic struct Foo x = { 0 };

C++ C 链接器 静态初始化

评论

0赞 Bodo 11/16/2023
引用 en.cppreference.com/w/cpp/language/siof如果一个翻译单元中的对象依赖于另一个翻译单元中已初始化的对象,则如果编译器决定以错误的顺序初始化它们,则可能会发生崩溃。我在此初始化中没有看到任何依赖于正在初始化的其他对象的内容。
0赞 blonded04 11/16/2023
@Bodo但是,如果假设某人依赖于已经构造,例如在另一个 TU 全局变量中:Foostatic void* foo = &x;
0赞 Bodo 11/16/2023
请参阅下面的评论回答。您评论中的添加内容属于问题。
0赞 Lundin 11/16/2023
修复标签并添加 C++ 标签。它适用于这里,因为正在比较 C 和 C++。

答:

6赞 dbush 11/16/2023 #1

C++ 中初始化的问题在于允许可执行代码在函数之前运行,因此并不总是清楚此类代码将以何种顺序运行。这在 C++ 中是必需的,因为静态对象的构造函数。main

另一方面,C 不允许代码在函数之外运行。静态对象的初始值设定项必须是常量表达式,可以在编译时计算。

这意味着像这样的初始化器在 C 中是完全可以的。static struct Foo x = { 0 };

评论

0赞 blonded04 11/16/2023
因此,C 完全可以放入另一个 TU 全局变量:,它会在运行时初始化,还是..?static void* foo = &x;
3赞 dbush 11/16/2023
@blonded04 静态变量的地址被视为常量表达式,在编译或链接时确定,因此没有问题。
1赞 Bodo 11/16/2023
的地址是一个常量表达式,将其分配给指针不需要初始化 的元素。任何可以直接或通过指针访问 元素的代码都将在初始化后运行。xxxfoox
2赞 BoP 11/16/2023
请注意,在 C 中可以正常工作的初始化在 C++ 中同样适用。只是“C++ 提供了更多的绳索来射击自己的脚”。
1赞 Lundin 11/16/2023 #2

对于具有静态(和线程)存储持续时间的对象,C 仅声明它们在调用 main() 之前的某个时间点初始化。C 只允许它们初始化为常量表达式。而在 C++ 中,对象可以具有构造函数,并且可以初始化为函数的结果。

如果我们在调用 main() 之前运行的“C 运行时”(CRT) 代码的“底层”,就变量而言,它只会初始化和 .从那里就可以开始了。等效的 C++ 运行时并不是那么简单,因为它还会启动构造函数调用等。由于 C++ 标准和程序员都没有指定特定的顺序,因此 CRT 只会以某种主观的外观顺序调用它们。如果此时对象之间存在初始化顺序依赖关系,则一切都将很快崩溃。.data.bss

C++ 还通过将静态初始化定义为适合两个子类别的所有内容来增加额外的复杂性:常量初始化零初始化。然后命名其他所有动态初始化(不要与动态分配混淆)。动态初始化反过来又带有出现顺序、排序等概念。

1赞 jxh 11/17/2023 #3

在您希望如何零初始化结构的上下文中,没有问题。

但是,在一般情况下,打开库(又名共享库)时,C代码中可能会出现问题。这是因为共享库可能包含一个代码段,该代码段在加载库时运行。.so.init

因此,您必须想象两个共享库,它们在初始化例程中相互引用彼此的数据结构。

诚然,这超出了 C 语言的范围。但是,在处理共享库时,它与链接器的上下文相关。

1赞 Brian Bi 11/17/2023 #4

C does not have the static initialization order fiasco. In C89, the rule was:

All the expressions in an initializer for an object that has static storage duration or in an initializer list for an object that has aggregate or union type shall be constant expressions.

So a static variable with scalar type could only be initialized with a single constant expression. If the variable's type is an array with a scalar element type, then each initializer would need to be a constant expression, and so on. Since a constant expression can neither produce side effects, nor depend on side effects produced by any other evaluation, changing the evaluation order of constant expressions doesn't affect the result. Furthermore, the compiler can simply emit already-initialized data (i.e., evaluate those constant expressions at compile time), so when the program starts, there is no static initialization to be done.

The only non-constant expressions that can be evaluated before are ones that are invoked from the C runtime. That's why the objects that are pointed to by , , and are already available for use by the first statement of , for example. Standard C doesn't allow users to register their own startup code to be run before —although GCC does provide an extension called (presumably named after the C++ feature) that you can use to recreate the static initialization order fiasco in C if you so desire.mainFILEstdinstdoutstderrmainmain__constructor__

Stroustrup wrote in The Design and Evolution of C++ that his aim was to make user-defined types usable wherever built-in types were. That meant that C++ had to allow global variables of class type, which means that their constructors would get called during program startup. Because early C++ didn't have functions, such constructor calls could never be constant expressions. And so, the static initialization order fiasco was born.constexpr

During the C++ standardization process, the question of the order in which to perform static initialization was a controversial topic. I think most people would agree that the ideal situation would be for every static variable to be initialized prior to its use. Unfortunately, that requires linker technology that didn't exist in those days (and probably still doesn't?). The initialization of a static variable can involve function calls, and those functions may be defined in another TU, which means you would need to perform whole-program analysis in order to successfully topologically sort the static variables in dependency order. It's worth noting that even if C++ could have been designed this way, it still wouldn't have completely prevented initialization order issues. Imagine if you had some library where a precondition of the function was that the function had been called at some point in the past. Then, if you have one static variable whose initializer calls and another whose initializer calls , then there's an order dependency that the compiler can't see.useinitinituse

Ultimately, the limited initialization order guarantees that we got in C++98 were the best that we could get under the circumstances. With the benefit of unlimited hindsight, perhaps one could have protested that the standard wouldn't be complete without functions (and that static variables should be required to only have constant initialization).constexpr