Is static initialization order fiasco applicable to C?

264 Views Asked by At

Everything I found across the internet about static initialization order fiasco was about C++, but is it true that if I initialize global variable of some type Foo like

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

I can't initialize variable of type struct Foo like static struct Foo x = { 0 };? if I want to get correct code because of SIOF?

4

There are 4 best solutions below

0
On BEST ANSWER

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 main are ones that are invoked from the C runtime. That's why the FILE objects that are pointed to by stdin, stdout, and stderr are already available for use by the first statement of main, for example. Standard C doesn't allow users to register their own startup code to be run before main—although GCC does provide an extension called __constructor__ (presumably named after the C++ feature) that you can use to recreate the static initialization order fiasco in C if you so desire.

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 constexpr functions, such constructor calls could never be constant expressions. And so, the static initialization order fiasco was born.

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 use function was that the init function had been called at some point in the past. Then, if you have one static variable whose initializer calls init and another whose initializer calls use, then there's an order dependency that the compiler can't see.

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 constexpr functions (and that static variables should be required to only have constant initialization).

4
On

The issue with initialization in C++ is that executable code is allowed to run before the main function, so it's not always clear in which order such code will run. This is necessary in C++ due to constructors for static objects.

C on the other hand does not allow code to be run outside of a function. Initializers for static objects must be constant expressions which can be calculated at compile time.

This means that an initializer such as static struct Foo x = { 0 }; is perfectly fine in C.

0
On

For objects with static (and thread) storage duration, C only states that they are initialized at some point before main() is called. C only allows them to be initialized to constant expressions. Whereas in C++, objects can have constructors and can be initialized to the result of a function.

If we peek "underneath the hood" of the "C runtime" (CRT) code that runs before main() is called, as far as variables go it will only initialize .data and .bss. From there it is ready to go. The equivalent C++ runtime is not that trivial, because it also launches constructor calls etc. Since no particular order is specified neither by the C++ standard nor the programmer, the CRT will just call them in some subjective order of appearance. If there are initialization order dependencies between objects at that point, everything will soon come crashing down.

C++ has also added additional complexity by defining static initialization as everything fitting into two sub-categories: constant initialization and zero-initialization. And then names everything else dynamic initialization (not to be confused with dynamic allocation). Dynamic initialization in turn comes with concepts of order of appearance, sequencing and so on.

0
On

In the context of how you want to zero-initialize your structure, there is no issue.

However, in the general case, the problem may occur in C code when opening .so libraries, AKA shared libraries. This is because shared libraries may include a .init code section that gets run when the library is loaded.

So, you have to imagine two shared libraries that refer to each other's data structures within their initialization routines.

Admittedly, this is outside the scope of the C language. However, it is relevant in the context of a linker when dealing with shared libraries.