アプリケーションのクラッシュに関連する C++ での静的初期化順序の大失敗について読みました。理解できたと思いますが、いくつか質問があります:
1) この問題を再現したい場合、どうすれば (プログラムがクラッシュするように) 再現できますか? クラッシュを再現するテストプログラムを書きたいと思います。可能であればソースコードを提供していただけますか?2) このC++ FAQ Lite
の記事
を読みましたが、2 つの異なるファイルに x と y の 2 つの静的オブジェクトがあり、y が x のメソッドを呼び出すと書かれています。グローバルな静的メンバーがファイル レベルのスコープを持っているため、どのように可能でしょうか?
3) この問題は非常に危険です。コンパイラ レベルで修正する試みはありますか?
4) あなたの C++ エキスパートは、実際のプロダクションでこの問題に何回直面しましたか?
3 に答える
編集:コメントに照らしてより正確になるように調整しました。
良い例は次のようになります。
// A.cpp
#include "A.h"
std::map<int, int> my_map;
// A.h
#include <map>
extern std::map<int, int> my_map;
// B.cpp
#include "A.h"
class T {
public:
T() { my_map.insert(std::make_pair(0, 0)); }
};
T t;
int main() {
}
問題は、インスタンスがオブジェクトt
の前に構築される可能性があることです。my_map
したがって、挿入はまだ構築されていないオブジェクトで発生する可能性があります。クラッシュの原因となります。
簡単な解決策は、代わりに次のようにすることです。
// A.h
#include <map>
std::map<int, int> &my_map()
// A.cpp
#include "A.h"
std::map<int, int> &my_map() {
// initialized on first use
static std::map<int, int> x;
return x;
}
// B.cpp
#include "A.h"
class T {
public:
T() { my_map().insert(std::make_pair(0, 0)); }
};
T t;
int main() {
}
関数を介して静的オブジェクトにアクセスすることにより、関数スコープの静的は最初の使用時に初期化されるため、初期化の順序を保証できます。したがって、t
オブジェクトが最初に構築さmy_map()
れ、最初の実行時に静的マップ オブジェクトを作成し、それへの参照を返します。
1) ランタイム スタートアップ コードを調べて、初期化の順序がどのように選択されるかを確認するか、少し実験する必要があります。2 つ以上のオブジェクト (おそらく 3 つまたは 4 つ) の間に初期化の依存関係を作成することで、エラーが発生する可能性を高めることができます。
2) ファイル レベルでオブジェクトをインスタンス化するだけです。
OBJECT_TYPE x;
3)私が知っている限り、これに対処するコンパイラはありません。リンク時またはリンク後に検出する必要があります。
4) 実際には、回避するのは簡単です: すべての初期化を自己完結型にします。
「1) この問題を再現したい場合、どうすれば (プログラムがクラッシュするように) できますか? クラッシュを再現するテスト プログラムを書きたいのですが、可能であればソース コードを提供していただけますか?」
移植可能なテスト ケースを作成することはできません。静的初期化順序の大失敗は、順序が定義されていないことです。この問題は、ある正当な順序で初期化すると機能するが、他の正当な順序で初期化すると失敗するコードを誰かが書いたときに発生します。したがって、それが機能することを保証できないのと同じ理由で、失敗することを保証することはできません。それが要点です。
おそらく、リンカはある翻訳単位のすべてのグローバルを別の翻訳単位のすべてのグローバルより前に初期化すると推測できます。したがって、A にグローバル A1 と A2、B にグローバル B1 と B2 を使用して、2 つのソース ファイル A と B をセットアップします。次に、コンストラクターで B1 を使用します (これは、「B1 が初期化されていない場合に失敗することを行う」という意味です)。 A1 のコンストラクターを作成し、B2 のコンストラクターで A2 を使用します。また、A2 のコンストラクターで A1 を使用します (そして、A でその順序で宣言します)。その場合、失敗しない唯一の順序は B1、A1、A2、B2 です。これは、実装が選択する可能性が非常に低いと思われるかもしれません。特定の実装で、何らかの形で成功した場合は、A2 を使用する B2 の代わりに B2 を使用するように切り替えて、初期化順序が変更されないことを願っています。
もちろん、初期化順序に関係なく失敗を保証するために、B1 のコンストラクターで B2 を使用 (および B でその順序で宣言) することもできます。しかし、それは静的な初期化順序の大失敗ではなく、根本的に壊れた循環依存関係にすぎません。
「2) この C++ FAQ Lite の記事を読みましたが、2 つの異なるファイルに x と y の 2 つの静的オブジェクトがあり、y が x のメソッドを呼び出すと書かれています。グローバルな静的メンバーがファイル レベルのスコープを持っているのはどうしてですか?」
たとえばextern
、両方の翻訳単位で宣言します (おそらく共通のヘッダーを使用します)。スコープ、リンケージ、保存期間はすべて別のものです。
「3) この問題は非常に危険です。コンパイラ レベルで修正する試みはありますか?」
私が知っていることではありません。オブジェクト X が (上記で定義した意味で) オブジェクト Y をそのコンストラクターで "使用" するかどうかを判断するのは停止中の問題であると確信しているため、リンク時に依存関係グラフを作成し、それを t ソートすると、部分的な測定が最善です。
「4) C++ のエキスパートであるあなたは、実際のプロダクションでこの問題に何回直面しましたか?」
決して、(a) グローバルをそのままにしておかないため、(b) グローバルを使用した場所では、初期化子で派手なことをすることを避けたからです。基本的に、クラスを設計してからそのグローバル インスタンスを持つことに決めないでください。グローバル オブジェクトを使用する場合は、それをグローバルとして設計します。また、可能な限り、グローバルな静的ではなく、ローカル スコープの静的を使用します。グローバルのように見えるものを提供する必要がある場合は、オブジェクトへの参照を返す関数として公開するか、スタック上に作成できるオブジェクトとして公開し、その関数を呼び出してプロキシとして機能します(または必要に応じてハンドル) グローバル状態の。スレッドの安全性についてはまだ心配する必要がありますが、スレッド化された環境はそれを管理する方法を提供します。
のようなグローバルを定義する API を実装している場合にのみ難しくなりますstd::out
。グローバルを宣言する同じヘッダーでダミーのファイルスコープ変数を定義する、使用できるトリックがあります。名前は思い出せないけど。