117
97

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-07-07

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

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

以上です。

117
97
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
117
97

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?