static variableの初期化順序

template structの中にstaticstd::mapを作ってmain()よりも前にアクセスしたらその時点ではまだコンストラクタが呼ばれておらず、中身がゼロ埋めされていてセグフォになった。

静的変数の初期化順序は基本的に書いた順、翻訳単位が違ったら実装定義、実装依存attributeがあって初期化順序のプライオリティを上げ下げできる、と言う程度のふわっとした知識しかなかったので、ちょっと調べてみよう。とはいえ今日は疲れているので規格ではなくcppreferenceを見てみる。とりあえず以下を読んで要約してみる。

ja.cppreference.com


staticな変数のことを知りたいので、「非ローカル変数」の項目を見よう。静的記憶域期間(static storage duration)を持つ変数は後述する例外を除いてmainよりも前に初期化される。その初期化には2段階あるそうだ。一つ目が静的初期化、もう一つが動的初期化

静的初期化が必ず動的初期化のに起きる。コンパイル時にstatic変数の値が決まる(i.e. constexprな)場合は、そのオブジェクトの値が実行バイナリに埋め込まれ、プログラムがロードされる時に展開される。そうでない場合、その変数に対応する領域はOSによってゼロ埋めされているので、結果としてオブジェクト全体がゼロ埋めされている状態になる。

イメージとしては、静的初期化はわざわざ何かを実行しているというよりは、「プログラムをロードする」ことそのものを指しているように見える。

動的初期化は必ず静的初期化のに起きる。つまり、constexprでない初期化式がここで実行される。これがどの順で実行されるかは、初期化する変数が何であるかに依存する。

1: 初期化する変数が明示的に特殊化されていないtemplateの時、その初期化順序(他の変数より先に初期化されるかどうか)は不定

マジかよ。終わりじゃん。解散。

template<typename T>
struct X {
    static T x;
};
template<typename T>
T X<T>::x; // これが初期化される順序は不定

template<typename T>
static T global_variable = function(); // これが初期化される順序も不定

2: 初期化する変数がC++17から入ったinline変数の場合、もし全ての翻訳単位で他の変数よりも先に定義されている場合、先に初期化される。includeの順序が入り乱れているときは不定になるが、常に他のものよりも先にincludeされているなら必ず先に初期化される、という条件付きの保証だ。

とはいえ、templatestatic変数よりも常に前に書いたとしても、templatestatic変数の初期化順序が不定なのでそこは保証されない。

3: 上記1, 2のどちらでもない場合、同じ翻訳単位内の値同士は書かれた順番に初期化される。翻訳単位が異なる変数のどちらが早く初期化されるかはは不定

これはよく聞く話だ。むしろここしか知られてなくないか。


さて、基本的にはこの順序になるらしいが、どうやら特例としてこれよりも早くして良いケースと、これより遅くしていいケースがあるようだ。特に遅くするケースはmainの最初の文よりも遅くなり得るらしい。

早期動的初期化が許される場合というのは、その初期化によって他のオブジェクトが変更されず、かつ他のオブジェクトを含めて静的に初期化しても動的に初期化しても同じ値になるような場合。このときは静的初期化にしてしまってよい。

難しいこと言ってるが、これは概ね as-if ルールと思ったら納得しやすい気がする。細かい動作に関しては最初に貼ったリンク先のページのサンプルコードが最強にわかりやすい。

遅延動的初期化が許される条件というのは、遅延してもその初期化が同じ翻訳単位でのいかなるstatic変数のODR-useよりも早くできる場合だ。このとき注意すべきなのは、その翻訳単位で定義されているstatic変数のうちのどれか一つがODR-useされる時には、全てのstatic変数が初期化されていなければならないことだ。

これもas-ifルールだと思うと納得しやすい。もしその翻訳単位内でstatic変数がどれ一つとして使用されないなら、そもそも初期化する必要はない。


というわけで初期化順序が概ねわかった。そして、以下のことがわかった。

template<typename T>
struct X {
    static T x;
};
template<typename T>
T X<T>::x; // これが初期化される順序は不定

これは普通に困る。

というわけで回避パターンを考えないといけないわけだ。関数内のstatic変数は関数が最初に呼ばれた時に初期化されることを考えると、以下のようなパターンが思い浮かぶ。

template<typename T>
struct X {
    static T& x() {
        static T y; // これは最初にX<T>::x()を呼んだ時に初期化されるはず
        return y;
    }
};

というようなパターンがいくつか、公式サイトのwikiで紹介されている。

isocpp.org