C++11の非静的メンバ変数初期化
こんにちは、tatsyです。
最近はすっかりC++11/14のすごさにやられています。
というわけで今回はC++11から追加されたクラスの非静的メンバ変数初期化について実験をしてみました。
非静的メンバ変数の初期化とは?
C++以外の言語、例えばJavaなどをやったことがある人なら、次のようなコードになじみがあると思います。
// A.java
public class A {
public int val = 1;
public A() {
}
void print() {
System.out.println(this.val);
}
}
注目していただきたいのは3行目です。3行目では、クラスのメンバ変数に対して初期値を与えています。
Javaだと当たり前のこの書き方ですが、C++03では次のようなコードはコンパイルエラーになります。
class Foo {
public:
int val = 1; // この行がエラー
public:
Foo() {
}
Foo(int v)
: val(v) {
}
};
それで、結局はコンストラクタ呼び出しの後でメンバ変数を初期化してあげなければいけないのでした。
ところが、C++11からは上記のような変数の初期化が非静的メンバ変数に限り可能になりました。すばらしい!!
さらに、今回は説明を省略しますがC++11にはデリゲート・コンストラクタ (MSDNに飛びます)という機能がありまして、これを使うと、あるコンストラクタの初期化に別のコンストラクタの定義を使用できます。
これらの機能によってC++11では、コンストラクタの定義がとても簡略化できるようになりました。
これで長々とコンストラクタの下にメンバ変数の初期化を書かなくて済む!!
非静的メンバ変数初期化の処理内容は?
さて、ここで一つ疑問が生じます。
「上記の方法で初期化したものをコンストラクタで上書きしたらどうなるんだろう?」
つまり、
class Foo {
public:
int val = 1;
public:
Foo() {
}
Foo(int v)
: val{v} {
}
};
みたいなコードを書いたときには変数vを取るコンストラクタでは何が起こるのだろうという疑問です。
考えられる挙動は2通りです。
- いったんvalを1で初期化した後に、コンストラクタに渡された変数で上書きする
- そもそもvalを1で初期化するコードはなかったことになり、コンストラクタに渡された変数で初期化する
これを調べるために、g++のオプションに-Sを付けて、アセンブリのコードを生成してあげたのが次のコードです(該当箇所だけを取り出し)。
まずは、コンストラクタを引数なしで呼び出した場合です。
call __main
leaq .LC0(%rip), %rcx
movl $1, %edx
大事なのは3行目で、確かに1が何等かの変数に代入されているようです。
では、次はFoo{2}という感じで、初期化変数と異なる変数を渡してみます。すると、次のようなアセンブリのコードが得られます。
call __main
leaq .LC0(%rip), %rcx
movl $2, %edx
ご覧のとおり、コンストラクタに渡した2が一度だけ変数に代入されています。
つまり、非静的メンバ変数初期化を使ったとしても、コンストラクタで該当の変数を初期化した場合には、そちらが優先され、なおかつ初期化は一度だけ行われるようです。
ということは、仮に非静的メンバ変数初期化で結構重たい初期化をしていたとしても、2回同じような初期化が実施されてしまうことはないということですね。
まとめ
という感じで記事を書いてみたのですが、実はコンストラクタでの初期化が非静的メンバ変数初期化を上書きされるという使用は実験するまでもなく明示されていたりします。
例えばcpprefernceには
If a member has a brace-or-equal initializer and also appears in the member initialization list in a constructor, the brace-or-equal initializer is ignored.
(もしメンバが中カッコやイコールで初期化されていて、なおかつコンストラクタにおけるメンバ変数の初期化リストにも表れる場合には、前者の中カッコやイコールでの初期化が無視される。
とあります。またEffective Modern C++にも同様のことが書かれていました。
というわけで、安心して非静的メンバ変数初期化が使うことができそうです。めでたし、めでたし。
今回も、最後までお読みいただきありがとうございました。