Risoluzione dell'ordine di inizializzazione statico Fiasco con C++20

Risoluzione dell'ordine di inizializzazione statico Fiasco con C++20

Secondo le FAQ di isocpp.org, l'ordine di inizializzazione statico fiasco "un modo sottile per mandare in crash il tuo programma". Le FAQ continuano:il problema dell'ordine di inizializzazione statico è un aspetto molto sottile e comunemente frainteso del C++. ". Oggi scrivo di questo aspetto molto sottile e frainteso del C++.

Il mio breve Disclaimer

Prima di continuare, voglio fare un breve disclaimer. Il post di oggi riguarda le variabili con durata di archiviazione statica e le loro dipendenze. Le variabili con durata di archiviazione statica possono essere variabili globali (spazio dei nomi), variabili statiche o membri di classi statiche. In breve, le chiamo variabili statiche. Le dipendenze da variabili statiche in diverse unità di traduzione sono, in generale, un odore di codice e dovrebbero essere un motivo per il refactoring. Di conseguenza, se segui il mio consiglio di refactoring, puoi saltare il resto di questo post.

Ordine di inizializzazione statico Fiasco

Le variabili statiche in un'unità di traduzione vengono inizializzate in base al loro ordine di definizione.

Al contrario, l'inizializzazione delle variabili statiche tra le unità di traduzione presenta un grave problema. Quando una variabile statica staticA è definita in un'unità di traduzione e un'altra variabile statica staticB è definita in un'altra unità di traduzione e staticB ha bisogno di staticA per inizializzarsi, si finisce con l'ordine di inizializzazione statico fiasco. Il programma non ha formato corretto perché non si ha alcuna garanzia su quale variabile statica venga inizializzata per prima in fase di esecuzione (dinamica).

Prima di parlare del salvataggio, lascia che ti mostri il fiasco dell'ordine di inizializzazione statico in azione.

Una possibilità 50:50 di farlo bene

Qual è l'unicità dell'inizializzazione delle variabili statiche? L'inizializzazione delle variabili statiche avviene in due fasi:statica e dinamica.

Quando non è possibile inizializzare un oggetto statico in fase di compilazione, viene inizializzato zero. In fase di esecuzione, l'inizializzazione dinamica avviene per questi elementi statici che vengono inizializzati da zero in fase di compilazione.

// sourceSIOF1.cpp

int quad(int n) {
 return n * n;
}

auto staticA = quad(5); 

// mainSOIF1.cpp

#include <iostream>

extern int staticA; // (1)
auto staticB = staticA;

int main() {
 
 std::cout << std::endl;
 
 std::cout << "staticB: " << staticB << std::endl;
 
 std::cout << std::endl;
 
}

La riga (1) dichiara la variabile statica staticA. L'inizializzazione di staticB dipende dall'inizializzazione di staticA. staticB è inizializzato da zero in fase di compilazione e inizializzato dinamicamente in fase di esecuzione. Il problema è che non vi è alcuna garanzia in cui l'ordine staticA o staticB viene inizializzato. staticA e staticB appartengono a diverse unità di traduzione. Hai una probabilità 50:50 che staticB sia 0 o 25.

Per rendere visibile la mia osservazione, cambio l'ordine dei collegamenti dei file oggetto. Questo cambia anche il valore di staticB!

Che fiasco! Il risultato dell'eseguibile dipende dall'ordine di collegamento dei file oggetto. Cosa possiamo fare quando non abbiamo C++20 a nostra disposizione?

Inizializzazione pigra di static con ambito locale

Le variabili statiche con ambito locale vengono create quando vengono utilizzate per la prima volta. L'ambito locale significa essenzialmente che la variabile statica è racchiusa in qualche modo tra parentesi graffe. Questa creazione pigra è una garanzia fornita da C++98. Con C++11, anche le variabili statiche con ambito locale vengono inizializzate in modo thread-safe. Il thread-safe Meyers Singleton si basa su questa garanzia aggiuntiva. Ho già scritto un post sull'"inizializzazione thread-safe di un singleton".

L'inizializzazione pigra può essere utilizzata anche per superare il fiasco dell'ordine di inizializzazione statico.

// 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 è, in questo caso, statico in un ambito locale (1). La riga (2) dichiara la funzione staticA, che viene utilizzata per inizializzare nella riga successiva staticB. Questo ambito locale di staticA garantisce che staticA venga creato e inizializzato durante il runtime quando viene utilizzato per la prima volta. La modifica dell'ordine dei collegamenti non può, in questo caso, modificare il valore di staticB.

Ora, risolvo il fiasco dell'ordine di inizializzazione statico usando C++20.

Inizializzazione in fase di compilazione di uno statico

Consentitemi di applicare constinit a staticA. constinit garantisce che staticA venga inizializzato durante la compilazione.

// 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) dichiara la variabile staticA. staticA (2) viene inizializzato durante la compilazione. A proposito, usare constexpr in (1) invece di constinit non è valido, perché constexpr richiede una definizione e non solo una dichiarazione.

Grazie al compilatore Clang 10, posso eseguire il programma.

Come nel caso dell'inizializzazione lazy con uno statico locale, staticB ha il valore 25.

Cosa c'è dopo?

C++20 presenta alcuni piccoli miglioramenti su modelli e Lambda. Nel prossimo post vi presento quali.


No