...
In this noncompliant code example, the initialization of foo
relies on bar
being previously initialized. However, bar
may be uninitialized at the point when the constructor for S
is called to construct foo
because the dynamic initialization is not ordered between translation unitsvalue of numWheels
in file1.cpp
relies on c
being initialized. However, because c
is defined in a different translation unit (file2.cpp
) than numWheels
, there is no guarantee that c
will be initialized by calling get_default_car()
before numWheels
is initialized by calling c.get_num_wheels()
. This is often referred to as the "static initialization order fiasco", and the resulting behavior is unspecified.
Code Block | ||||
---|---|---|---|---|
| ||||
// Filefile.h struct S { S(); void f(); }; #ifndef FILE_H #define FILE_H class Car { int numWheels; public: Car() : numWheels(4) {} explicit Car(int numWheels) : numWheels(numWheels) {} int get_num_wheels() const { return numWheels; } }; #endif // FILE_H // File1file1.cpp #include "Filefile.h" S#include bar;<iostream> S::S() { bar.fextern Car c; int numWheels = c.get_num_wheels(); } // File2.cpp #include "File.h" S foo;int main() { std::cout << numWheels << std::endl; } // File3file2.cpp #include "Filefile.h" extern S foo; extern S bar; int mainCar get_default_car() { return foo.fCar(6); } Car bar.f(); }c = get_default_car(); |
Implementation Details
The value printed to the standard output stream will often rely on the order the translation units are linked. For instance, with Clang 3.8.0 on x86 Linux, the command clang++ file1.cpp file2.cpp && ./a.out
will write 0
while clang++ file2.cpp file1.cpp && ./a.out
will write 6
.
Compliant Solution
This compliant solution ensures that both foo
and bar
are initialized in order by placing their definition in the same translation unituses the "construct on first use" idiom to resolve the static initialization order issue. Because the numWheels
object was moved into the body of a function, its initialization is guaranteed to happen when control flows over the point of declaration, ensuring control over the order. The global object c
is initialized before execution of main()
begins, so by the time get_num_wheels()
is called, c
is guaranteed to have already been dynamically initialized.
Code Block | ||||
---|---|---|---|---|
| ||||
// File.h struct S { S(); void f(); }; // File1file.h #ifndef FILE_H #define FILE_H class Car { int numWheels; public: Car() : numWheels(4) {} explicit Car(int numWheels) : numWheels(numWheels) {} int get_num_wheels() const { return numWheels; } }; #endif // FILE_H // file1.cpp #include "Filefile.h" extern S bar; S::S#include <iostream> int &get_num_wheels() { extern Car c; static int numWheels = bar.f()c.get_num_wheels(); return numWheels; } // File2.cpp #include "File.h" S bar; // Ensure that bar is initialized before foo. S foo;int main() { std::cout << get_num_wheels() << std::endl; } // File3file2.cpp #include "Filefile.h" extern S foo; extern S bar; int main Car get_default_car() { return foo.fCar(6); } Car bar.fc = get_default_car(); } |
Risk Assessment
Recursively reentering a function during the initialization of one of its static objects can result in an attacker being able to cause a crash or denial of service. Indeterminately ordered dynamic initialization can lead to undefined behavior due to accessing an uninitialized object.
...
[ISO/IEC 14882-2014] | Subclause 3.6.2, "Initialization of Non-local Variables" |
...