C++
C++11

C++11 Universal Initialization は、いつでも使うべきなのか

More than 3 years have passed since last update.

答えは、現状 "No." です。

ではいつ使うべきで、いつ使わないべきか?

以下の記事を元ネタに、まとめてみます。


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 をつけることで、コンパイラが不束者をキャッチします。


意味不鮮明な暗黙キャストを抑止する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パターンなど。


要素1個でのコンテナ初期化

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 のつもりだったかもしれないが、intstring の要素は初期化できない。コンパイラは「int はサイズのつもりじゃね?」と、サイズ指定のコンストラクタと結びつけてしまう。残念なことに コンパイルエラーにならず、実行時に初めて間違いに気づくかも知れません。普通しない類の間違いですが、intstringではなく、もっと複雑なクラス階層だとミスに気づかないかもしれない。


【混沌の原因】

この混沌は {...} が、これまでのコンストラクタとinitializer_list<value_type> を引数にとる新種のコンストラクタと、どちらも呼び出す可能性がある(引数次第)という特性に起因しています。

故に、通常のコンストラクタとinitializer_listのコンストラクタと、両方を考慮しなければならない 被りパターン では、{...} の利用を避けたほうがバグが少なくなるかもしれません。


被りパターンを避ける|2行だけど、こちらの方が可読性良い?

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 だったので深刻さはありませんが、もっと複雑なケースだと辛くなってきます。

元ネタの記事に、以下の例がありました。


駄目なmake_uniqueの実装例

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}


「要素による初期化」であると自明でないなら、昔ながらに (...) を使う方が安全かつ安心。という方針で書き改めてみます。


ベターなmake_uniqueの実装例

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::normstd::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} は冗長に思います。そんなコード書く人、いるかわかりませんが。

基本型に関する自分向けガイドラインは、「どちらでも良い/既存コードのスタイルに合わせる。」としておきたいと思います。

以上です。