答えは、現状 "No." です。
ではいつ使うべきで、いつ使わないべきか?
以下の記事を元ネタに、まとめてみます。
- Is C++11 Uniform Initialization a replacement for the old style syntax? --- StackExchange
- When to use the brace-enclosed initializer? --- StackOverflow
- Why can't I initialize a reference in an initializer list with uniform initialization? --- StackOverflow
- Uniform initialization syntax and semantics --- Stroustrup.com
1. はじめに|Universal Initialization とは?
C++11 からは int
変数でも、配列でも、構造体でも、std::vector
のようなSTLコンテナでも、同様の見た目で初期化できます。例えば、昔ながらの書き方では・・・
int x = 3; // "=" を使って。
int a[] = { 0, 1, 2, 3 }; // "= {}" を使って。
struct S1 { int a, b; } s = { 0, 1 }; // これも "= {}" を使って。
std::vector<int> v;
for(int i = 0; i < 4; ++i) v.push_back(a[i]); // ループを使って。
初期化する対象によってバラバラですが、C++11なら・・・
int x { 3 };
int a[] { 0, 1, 2, 3 };
struct S1 { int a, b; } s { 0, 1 };
std::vector<int> v { 0, 1, 2, 3 };
この {
... }
を使った統一的な書き方は、ゆえに Universal Initialization、または Uniform Initialization と呼ばれます。Bjarne Stroustrup 推奨の初期化方法だそうです。
2. 有用なケース
2.1 コンテナや配列を要素で初期化
この使い方が、{...}
の使い方の王道でしょうか。
std::vector<int> v;
for(int i = 0; i < 4; ++i) v.push_back(a[i]);
int a[] = { 0, 1, 2, 3 };
std::vector<int> v { 0, 1, 2, 3 };
int a[] { 0, 1, 2, 3 };
新しい書き方はダイレクトで、見た目がわかりやすくなった。そして、オペレータ =
は 右辺のオブジェクトを左辺にコピー する意味合いがあるので、a[]
の初期化に =
を使わない点も、より直感とマッチするように思います。
2.2 冗長な戻り値の型を省略できる
構造体を返す関数での使い方。
vec3 GetValue()
{
// ここで x,y,z を計算する
return vec3(x, y, z);
}
vec3 GetValue()
{
// ここで x,y,z を計算する
return {x, y, z}; // vec3{x,y,z}で生成されたオブジェクトが返る
}
関数の最初で vec3
と言っているのだから、return
で再び vec3
というのは冗長という考え。
ただし、巨大な関数では最初の vec3
が視界の外に行ってしまい、可読性が落ちるという意見もあるようです。
また、昔ながらの構造体ではなく、コンストラクタに explicit
が付いているクラスでは、この暗黙キャストは拒否られてコンパイルできません。
std::tuple<int,char> ReturnMySpecialTuple(void)
{
return { 1, 'a' }; // 暗黙キャストは許すまじ; Compile error
}
普通、explicit
をコンストラクタに付けるのは、そのコンストラクタが、暗黙キャストのためにうっかり呼ばれるのを防止するため。
例えば・・・
// The C++ Programming Language 4th ed
// 16.2.6 explicit Constructors より引用(一部改変)
class Date {
int d, m, y;
public:
Date(int d_=today.d, int m_=today.m, int y_=today.y)
: d(d_), m(m_), y(y_) {} // 良い子は値をチェックしてね
}
void DoSomethingOnDate(Date d)
{
// ... なにか価値あることを提供 ...
}
DoSomethingOnDate(15); // 15 なんのこっちゃ。
Date d = 15; // 難読性コーディングのつもり?
explicit
をつけることで、コンパイラが不束者をキャッチします。
class Date {
int d, m, y;
public:
explicit Date(int d_=today.d, int m_=today.m, int y_=today.y)
: d(d_), m(m_), y(y_) {} // 良い子は値をチェックしてね
}
DoSomethingOnDate(15); // Compile error
Date d = 15; // Compile error
Date d(15); // 明示的な初期化だから、今まで通り可能(第一引数 d_ = 15)
このように仕組みだけ見ると有用な explicit
ですが、歴史的経緯により、一部クラスに対して、余計な explicit
がついてしまったようです。
変な挙動に巻き込まれ時間を無駄にしないためには、戻り値に暗黙キャストの {...}
を使うのは、昔ながらの構造体のみにした方が良いかもしれない。
3. 使わない方が無難なケース
3.1 要素1個でのコンテナ初期化
混乱を招きがちな使い方。特に以下の3パターンなど。
std::vector<int> v(10); // サイズ10の int のベクタ、全要素 0
std::vector<int> w{10}; // サイズ1の int のベクタ、最初の要素 10
std::vector<std::string> s{10}; // サイズ10の string のベクタ、全要素は空
一行目は明らか。昔からあるサイズ指定のコンストラクタが呼ばれる。
二行目は Universal Initialization のお陰で、直接要素を与えての初期化されます。C++11 では std::vector
のコンストラクタにinitializer_list<value_type>
を第一引数に取るものが定義されており、これを呼び出します。今回、たまたま1要素しかなかったため、引数1個のコンストラクタ(一行目)と見た目が近くなってしまった。
三行目は、Universal Initialization のつもりだったかもしれないが、int
で string
の要素は初期化できない。コンパイラは「int
はサイズのつもりじゃね?」と、サイズ指定のコンストラクタと結びつけてしまう。残念なことに コンパイルエラーにならず、実行時に初めて間違いに気づくかも知れません。普通しない類の間違いですが、int
とstring
ではなく、もっと複雑なクラス階層だとミスに気づかないかもしれない。
【混沌の原因】
この混沌は {...}
が、これまでのコンストラクタとinitializer_list<value_type>
を引数にとる新種のコンストラクタと、どちらも呼び出す可能性がある(引数次第)という特性に起因しています。
故に、通常のコンストラクタとinitializer_list
のコンストラクタと、両方を考慮しなければならない 被りパターン では、{...}
の利用を避けたほうがバグが少なくなるかもしれません。
std::vector<int> v(10); // サイズ10の int のベクタ、全要素 0
v[0] = 0; // 最初の要素のみの初期化
もしくは、自分のクラスなら、initializer_list
のコンストラクタを書かなければ、被りパターンも発生しえないので {...}
も安心です。
3.2 「明らかに要素」でないものによる初期化
{...}
は普通のコンストラクタも呼び出せるので、要素でない値を元にした初期化もできます。例えば、前述のコンテナサイズがそうでした。
しかし、元々 {...}
は「配列や構造体の中身を列挙して初期化」する文法です。それ以外のデータを元にオブジェクトを構築するのは、むしろ、(...)
を使ったこれまでのコンストラクタの本来の機能です。そこに {...}
を持ち込むのは混乱の元。
std::string good ("abcde", 3); // OK! 昔ながらのコンストラクタ
std::string pretty { 'a', 'b', 'c' }; // OK! 要素による初期化
std::string bad { "abcde", 3 }; // OMG! 要素じゃない
この例は単純な std::string
だったので深刻さはありませんが、もっと複雑なケースだと辛くなってきます。
元ネタの記事に、以下の例がありました。
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T> {
new T { std::forward<Args>(args)... } // argsは要素?それ以外?
};
}
auto q = make_unique<std::vector<string>>(20, "x"); // string 20個分 {"x","x",...}
auto p = make_unique<std::vector<int>>(20, -1); // int 2個分! {20,-1}
「要素による初期化」であると自明でないなら、昔ながらに (...)
を使う方が安全かつ安心。という方針で書き改めてみます。
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T> { // 要素の列挙による初期化なので {...}
new T (std::forward<Args>(args)...) // 昔ながらのコンストラクタ
};
}
auto q = make_unique<std::vector<string>>(20, "x"); // string 20個分
auto p = make_unique<std::vector<int>>(20, -1); // int 20個分 :-)
期待通りの、統一された振る舞いになりました。
3.3 引数の型を省略
構造体を引数に取る関数で、型を省略するのに {...}
を使う用途です。
double norm(const std::complex<double> &c)
{ return c.real()*c.real() + c.imag()*c.imag(); }
std::complex<double> two_plus_twoi {2.0,2.0};
std::cout << norm(two_plus_twoi); // OK! "8" を表示
double norm(const std::complex<double> &c)
{ return c.real()*c.real() + c.imag()*c.imag(); }
std::cout << norm({2.0,2.0}); // OK! "8" を表示
要素を列挙して、暗黙キャストで一時オブジェクト 2.0+2.0i
を作り、関数に渡しています。楽になって良いじゃない。しかし、以下のコードはうまく行きません。
std::cout << std::norm({2.0,2.0}); // NG! コンパイルエラー
原因は std::norm
が std::norm(const std::complex<T> &c)
という風にテンプレート定義されており T == double
の推定に失敗するから。南無三。
ある関数の実装が「テンプレートかそうでないか」というのは、利用側は意識しないで済む方が良いので、{...}
による暗黙キャストでトラブルに巻き込まれるより、現状は利用を避けるほうが良いでしょう。
また、開発途中で API が流動的だったり、色々と多重定義されている場合、{...}
がたまたま希望と違うコンストラクタにマッチし、うっかりコンパイル通ってしまう危険性もあります。
暗黙のキャストは「絶対明らか」な場合以外、避けた方が良いと思います。
3.3 参照変数の初期化
以下のコードは、VisualStudio 2013 では予想通りの動作ですが、CentOS7.1 デフォルトの gcc 4.8 ではコンパイルできませんでした。
std::vector<int> v(10); // 10要素を持つ int のベクタ
int &t{v[3]}; // 4要素目に x という名前を付ける: compile error
参照変数は、当面、昔ながらに書いたほうが平和に暮らせそう。
std::vector<int> v(10); // 10要素を持つ int のベクタ
int &t = v[3]; // 4要素目に x という名前を付ける: OK!
クラスのコンストラクタでは =
は利用できないので (...)
ですね。
class S {
int &x;
public:
S(int &x_) : x(x_) {} // 昔ながらの参照変数の初期化
int get(void) { return x; }
};
int x = 0;
S s(x);
std::cout << "x:" << s.get() << std::endl; // "0" を表示
x = 1;
std::cout << "x:" << s.get() << std::endl; // "1" を表示
4. 結論|ガイドライン
結局、いつ {...}
を使うべきか。
StackOverflow にあったガイドラインを、自分流にアレンジしてみました。
対象 | 初期化のパターン |
---|---|
昔ながらの配列 | 要素で初期化は {...} でOK |
昔ながらの構造体 | 要素で初期化は {...} でOK |
昔ながらのクラス | 要素で初期化するなら {...}
|
要素以外で初期化するなら (...)
|
|
STLコンテナ | 要素複数で初期化するなら {...}
|
要素1個なら v[0] = value 等がベター |
|
initializer_list コンストラクタを持つクラス一般 |
他のコンストラクタと引数の数が被らない範囲の要素数なら {...}
|
被るなら各要素を個別に初期化 | |
そもそも要素でないなら (...) で初期化すべし |
|
参照変数 |
t = ref または (...) で初期化 |
戻り値オブジェクト | 要素を詰めて返すなら {...}
|
それ以外なら、型明示 (...) で初期化 |
|
引数オブジェクト | 型明示 (...) で初期化 |
さて、int x
のような基本型はどちらがいいの?というのには触れていません。些細な違いはありますが、現状どちらでも良いように思います。
int x { 0 }
の方が文法に統一感ありますが int x = 0;
や auto x = 0;
の方が見やすいという意見もありそう。ただ、int x = {0}
は冗長に思います。そんなコード書く人、いるかわかりませんが。
基本型に関する自分向けガイドラインは、「どちらでも良い/既存コードのスタイルに合わせる。」としておきたいと思います。
以上です。