C++20 による静的初期化順序の大失敗の解決

isocpp.org の FAQ によると、静的な初期化順序の大失敗は「プログラムをクラッシュさせる微妙な方法」です。 FAQ の続き:静的な初期化順序の問題は、C++ の非常に微妙でよく誤解されている側面です。 ". 今日は、C++ のこの非常に微妙で誤解されている側面について書きます。

簡単な免責事項

先に進む前に、短い免責事項を述べたいと思います。今日の投稿は、静的ストレージ期間とその依存関係を持つ変数に関するものです。静的ストレージ期間を持つ変数は、グローバル (名前空間) 変数、静的変数、または静的クラス メンバーの場合があります。要するに、私はそれらを静的変数と呼んでいます。 異なる翻訳単位での静的変数への依存は、一般にコードの臭いであり、リファクタリングの理由となるはずです。 したがって、私のアドバイスに従ってリファクタリングする場合は、この投稿の残りをスキップできます。

静的初期化命令の失敗

1 つの翻訳単位の静的変数は、定義順序に従って初期化されます。

対照的に、翻訳単位間の静的変数の初期化には深刻な問題があります。 1 つの静的変数 staticA が 1 つの翻訳単位で定義され、別の静的変数 staticB が別の翻訳単位で定義され、staticB が自身を初期化するために staticA を必要とする場合、静的初期化順序の大失敗で終了します。実行時にどの静的変数が最初に初期化されるか (動的) が保証されていないため、プログラムの形式が正しくありません。

レスキューについて話す前に、静的な初期化命令の失敗が実際に起きている様子をお見せしましょう。

正解する 50:50 のチャンス

静的変数の初期化の特徴は何ですか?静的変数の初期化は、静的と動的の 2 つのステップで行われます。

コンパイル時に static を const で初期化できない場合は、ゼロで初期化されます。実行時に、コンパイル時にゼロで初期化されるこれらの static に対して動的な初期化が行われます。

// 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;
 
}

行 (1) は、静的変数 staticA を宣言します。 staticB の初期化は、staticA の初期化に依存します。 staticB は、コンパイル時にゼロで初期化され、実行時に動的に初期化されます。問題は、staticA または staticB が初期化される順序が保証されないことです。 staticA と staticB は異なる翻訳単位に属します。 staticB が 0 または 25 である確率は 50:50 です。

観察結果を可視化するために、オブジェクト ファイルのリンク順序を変更します。これにより、staticB の値も変更されます!

なんと大失敗!実行可能ファイルの結果は、オブジェクト ファイルのリンク順によって異なります。 C++20 を自由に使用できない場合、何ができるでしょうか。

ローカル スコープによる静的の遅延初期化

ローカル スコープを持つ静的変数は、最初に使用されるときに作成されます。ローカル スコープは基本的に、静的変数が何らかの方法で中かっこで囲まれていることを意味します。この遅延作成は、C++98 が提供する保証です。 C++11 では、ローカル スコープの静的変数もスレッド セーフな方法で初期化されます。スレッドセーフな Meyers Singleton は、この追加の保証に基づいています。 「シングルトンのスレッドセーフな初期化」については、すでに記事を書いています。

遅延初期化は、静的初期化順序の大失敗を克服するためにも使用できます。

// 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 はローカル スコープの static です (1)。行 (2) は、次の行 staticB で初期化するために使用される関数 staticA を宣言します。この staticA のローカル スコープにより、実行時に初めて使用されるときに staticA が作成され、初期化されることが保証されます。この場合、リンクの順序を変更しても、staticB の値は変更されません。

ここで、C++20 を使用して静的初期化順序の大失敗を解決します。

スタティックのコンパイル時の初期化

constinit を staticA に適用させてください。 constinit は、コンパイル時に staticA が初期化されることを保証します。

// 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) 変数 staticA を宣言します。 staticA (2) はコンパイル時に初期化されます。ちなみに、constinit の代わりに (1) で constexpr を使用することは無効です。constexpr には宣言だけでなく定義が必要だからです。

Clang 10 コンパイラのおかげで、プログラムを実行できます。

ローカル static を使用した遅延初期化の場合と同様に、staticB の値は 25 です。

次は?

C++20 では、テンプレートとラムダに関していくつかの小さな改善が行われています。次の投稿では、どれを紹介します。


No