Die Lösung des Static Initialization Order Fiasco mit C++20
Laut isocppp.org handelt es sich bei diesem Fiasko um eine subtile Möglichkeit, ein C++-Programm zum Absturz zu bringen. Um diesen missverstandenen Aspekt geht es in diesem Artikel.
Dem Wortlaut der FAQ von isocppp.org [1] folgend, ist das Static Initialization Order Fiasco "a subtle way to crash your program". Die FAQ geht noch weiter: "The static initialization order problem is a very subtle and commonly misunderstood aspect of C++." Genau mit diesem subtilen und missverstandenen Aspekt von C++ befasst sich mein heutiger Artikel.
Bevor ich beginne, möchte ich einen kurzen Disclaimer loswerden. In diesem Artikel geht es um Variablen mit statischer Speicherdauer und ihren Abhängigkeiten. Sie können globale oder statische Variablen beziehungsweise statische Klassenmitglieder sein. Der Einfachheit halber werde ich sie statische Variablen nennen. Abhängigkeiten statischer Variablen in verschiedenen Übersetzungseinheiten sind im Allgemeinen ein Geschmäckle (code smell) und sollten die Grundlage für die Refaktorierung des Codes sein. Wenn du daher deinen Code refaktorierst, erübrigt sich für dich der Rest des Artikels.

Static Initialization Order Fiasco
Statische Variablen in einer Übersetzungseinheit werden in ihrer Definitionsreihenfolge initialisiert. Im Gegensatz dazu besitzt die Initialisierungsreihenfolge von statischen Variablen zwischen Übersetzungseinheiten ein großes Problem. Wenn eine statische Variable staticA
in einer Übersetzungseinheit definiert wird, deren Initialisierung von der Initialisierung einer statischen Variable staticB
in einer anderen Übersetzungseinheit abhängt, endet das im Static Initialization Order Fiasco. Das Programm ist ill-formed, denn es definiert nicht, welche statische Variable zuerst zur Laufzeit (dynamisch) initialisiert wird.
Bevor ich mich der Lösung dieses Problems widme, möchte ich erst das Static Initialization Order Fiasco in Aktion vorstellen.
Eine 50:50-Chance auf Korrektheit
Warum ist die Initialisierung einer statischen Variable besonders? Sie besteht aus zwei Schritten: einem statischen und einem dynamischen Schritt. Wenn eine statische Variable nicht zur Compilezeit const-initialisiert [2]werden kann, wird sie vorerst null-initialisiert [3]. Zur Laufzeit werden dann die statischen Variablen initialisiert, die zur Compilezeit null-initialisiert [4] wurden:
// sourceSIOF1.cpp
int quad(int n) {
return n * n;
}
auto staticA = quad(5);
// mainSOIF1.cpp
#include <iostream>
extern int staticA;
auto staticB = staticA;
int main() {
std::cout << std::endl;
std::cout << "staticB: " << staticB << std::endl;
std::cout << std::endl;
}
Zeile (1) erklärt eine statische Variable staticA
. Die Initialisierung von staticB
hängt von der Initialisierung von staticA
. staticB
wird zur Compilezeit null-initialisiert und zur Laufzeit dynamisch initialisiert. Das Problem ist, dass es keine Garantie gibt, ob zuerst staticA
oder staticB
initialisiert wird, da staticA
und staticB
zu verschiedenen Übersetzungseinheiten gehören. Nun hast du eine 50:50-Chance, dass staticB
0 oder 25 ist.
Um meine Beobachtungen zu verdeutlichen, habe ich die Linkreihenfolge der Objektdateien geändert. Damit ändert sich auch der Wert von staticB
!

Was für ein Fiasko! Das Ergebnis des Programms hängt von der Linkreihenfolge der Objektdateien ab. Wie lässt sich das Problem lösen, wenn wir nicht auf C++20 zurückgreifen können?
Verzögerte Initialisierung einer statischen Variable in einem lokalen Bereich
Statische Variable in einem lokalen Bereich werden erst erzeugt, wenn sie benötigt werden. Der lokale Bereich meint im Wesentlichen, dass die statische Variable innerhalb geschweifter Klammern verwendet wird. Diese verzögerte Initialisierung (lazy initialization) ist eine Garantie von C++98. Mit C+11 werden statische Variable in einem lokalen Bereich darüber hinaus noch Thread-sicher initialisiert. Das Thread-sichere Meyers' Singleton basiert auf dieser Garantie. Ich habe bereits einen Artikel "Thread-sicheres Initialisieren eines Singletons [5]" geschrieben.
Die verzögerte Initialisierung kann als Lösung für das Static Initialization Order Fiasco verwendet werden:
// sourceSIOF2.cpp
int quad(int n) {
return n * n;
}
int& staticA() {
static auto staticA = quad(5); // (1)
return staticA;
}
// mainSOIF2.cpp
#include <iostream>
int& staticA(); // (2)
auto staticB = staticA(); // (3)
int main() {
std::cout << std::endl;
std::cout << "staticB: " << staticB << std::endl;
std::cout << std::endl;
}
staticA
ist eine statische Variable in einem lokalen Bereich (1). Die Zeile (2) erklärt die Funktion staticA
, die zum Einsatz kommt, um die statische Variable staticB
zu initialisieren. Der lokale Bereich von staticA
sichert zu, dass staticA
genau dann von der Laufzeit erzeugt und initialisiert wird, wenn diese das erste Mal genutzt wird. In diesem Fall besitzt die Linkreihenfolge keine Auswirkung auf den Wert von staticB
.

Zum Abschluss möchte ich noch das Static Initialization Order Fiasco mithilfe von C++20 lösen.
Compilezeit-Initialisierung einer statischen Variable
Ich werde constinit
auf staticA
anwenden. Ersteres sichert zu, dass staticA
zur Compilezeit initialisiert wird:
// sourceSIOF3.cpp
constexpr int quad(int n) {
return n * n;
}
constinit auto staticA = quad(5); // (2)
// mainSOIF3.cpp
#include <iostream>
extern constinit int staticA; // (1)
auto staticB = staticA;
int main() {
std::cout << std::endl;
std::cout << "staticB: " << staticB << std::endl;
std::cout << std::endl;
}
(1) deklariert die Variable staticA
. Diese (2) wird zur Compilezeit initialisiert. Eine kleine Beobachtung finde ich noch interessant. constexpr
anstelle von constinit
in (1) zu verwenden, ist nicht gültig, da constexpr
eine Definition und nicht nur eine Deklaration benötigt.
Dank des Clang-10-Compilers kann ich das Programm ausführen:

Wie im Fall der verzögerten Initialisierung durch statische Variablen in einem lokalen Bereich ist der Wert von staticB
immer 25.
Wie geht' s weiter?
C++20 besitzt einige kleine Verbesserungen rund um Templates und Lambdas. Genau darüber werde ich in meinem nächsten Artikel schreiben.
( [6])
URL dieses Artikels:
https://www.heise.de/-4847039
Links in diesem Artikel:
[1] https://isocpp.org/wiki/faq/ctors#static-init-order
[2] https://en.cppreference.com/w/cpp/language/constant_initialization
[3] https://en.cppreference.com/w/cpp/language/zero_initialization
[4] https://en.cppreference.com/w/cpp/language/zero_initialization
[5] https://www.grimm-jaud.de/index.php/blog/threadsicheres-initialisieren-eines-singletons
[6] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien