マニアックな仕様の話とかではなく結構実用的な話。
C++のコンストラクタにはメンバ初期化子という仕様がある。コロンとか使うアレ。
class MyClass {
int a;
double b;
string s;
MyClass() : a(123), b(3.14), s("hello") {}
};
こんな感じで各メンバ変数に対しコンストラクタを呼び出せる。
これがメンバ変数ではなく普通の関数内で宣言したローカル変数ならば宣言時にカッコを付ければコンストラクタが呼び出せるが、クラスのメンバ変数という場合においては波かっこ内にあるコンストラクタの コードが始まる頃にはメンバ変数は既に宣言されており、コンストラクタを呼び出すチャンスがない。この問題に対する救済として用意されている仕様だろうと思われる。
ここで問題になるのが初期化順序である。 以下のようなコードを考えてみよう。
class MyClass{
int a;
int b;
MyClass() : a(10), b(2*a+1) {}
};
bの初期化処理はaに依存している。つまり仮にa→bという順序で初期化が行われるなら問題は起きないが、b→aという順序で初期化が行われた場合bはでたらめな値で初期化されてしまう。我々はこの順序をどう制御したらいいのか?
結論から言うと、クラス宣言においてメンバ変数を書く順序によって初期化の順序は決まる。
class MyClass1{
int a;
int b;
MyClass1() : a(10), b(2*a+1) {} // a→bの順で初期化される
};
class MyClass2{
int b;
int a;
MyClass2() : a(10), b(2*a+1) {} // b→aの順で初期化、コンパイラによっては警告が出る
};
初期化順序は初期化子リストの順番とは一切関係がない。MyClass() : a(), b()
と書いてもMyClass() : b(), a()
と書いても同じ結果になる。
と、ここで一つの疑問が湧く。 メンバ変数の初期化が書かれた順序で行われるならば、解体はその逆の順番で行われて欲しいものだ。
例えば「AはBに依存している」という場合。 初期化は当然B→Aという順序で行うが、解体処理で先にBを壊してしまうと、Aを解体するときに依存しているBに対してしかるべきやり取りが出来なくなってしまう。 だから一般論として、モジュール群の初期化と破壊はそれぞれ逆順で行うのが正しい。C++クラスのメンバ変数の初期化と破壊処理はその通りになっているか?
VC++でこんな感じのコードを書いて確かめてみた。
class MySub2 {
public:
MySub2() {
cout << "Mysub2" << endl;
}
~MySub2() {
cout << "Mysub2 d" << endl;
}
};
class MySub1 {
public:
MySub1(MySub2& hoge) {
cout << "Mysub1" << endl;
}
~MySub1() {
cout << "Mysub1 d" << endl;
}
};
class MyClass {
MySub2 sub2;
MySub1 sub1;
public:
MyClass() : sub2(), sub1(sub2) {
cout << "MyClass" << endl;
}
void hello() {
cout << "Hello" << endl;
}
~MyClass() {
cout << "MyClass d" << endl;
}
};
int main(int argc, char *argv[])
{
MyClass obj;
obj.hello();
}
出力結果は以下のようになった。
Mysub2
Mysub1
MyClass
Hello
MyClass d
Mysub1 d
Mysub2 d
期待通り逆順で壊されることが分かった。
以上より、C++のクラスにおいて「メンバ変数を書く順番」とは単にメモリ上の配置の順番やソースの見やすさなどを決めるものではなく、メンバ同士の依存関係を表す大いに重要な要素であることが分かった。
謝辞
発端となった僕のTeratailでの質問に答えて下さったBearded-Ockhamさん ありがとうございました。