🙆

[C++]波括弧初期化(uniform initialization)はメリットが多い

2023/04/23に公開

最近C++のuniform initializationという便利な変数初期化の機能を知ったのでアウトプットしてみる。

uniform initializationというのは以下のように波括弧を使って変数初期化やコンストラクタの呼び出しをできる初期化方法だ。

int test{100}; // 100で初期化
std::string test1{}; // コンストラクタ呼ばれ空文字列で初期化
std::vector<int> test2{}; // コンストラクタ呼ばれ空のリストで初期化

この波括弧の初期化はC++11から使えるようになっていた機能らしい。
しばらくC++から離れていた老いぼれプログラマーの私は恥ずかしながら知らなかった。。

ということで、変数宣言のすぐ後に{}を付けることで変数の初期化がカンタンにできるuniform initializationについて自分なりにメリットを整理してみた。

メリット

(1). メンバ変数の初期化でコンストラクタ定義不要

今までこの波括弧初期化を知らなかったので、クラスのメンバ変数の初期化はわざわざコンストラクタを定義して以下のようにメンバ初期化子リストで各メンバ変数を初期化するやり方をしていた。

Sample::Sample() : test(100), test2("hoge")

uniform initializationを使えばメンバ初期化のためにわざわざコンストラクタを定義する必要も無い。

class Sample
{
  // Sample(); // 定義不要
  int test{100};
  std::string test2{"hoge"};
};

(2). 初期化漏れが分かりやすい

コンストラクタのメンバ初期化子で初期化する場合、メンバ変数の宣言と初期化の場所が違うためたまに初期化忘れをしてしまうことがあった。
以下のようにメンバ変数が増えてくると結構漏らしてしまっていた。

Sample::Sample() : test(100), test2("hoge"), test3("hoge1"), test4("hoge2"), test5("hoge3")

uniform initializationなら宣言と初期化を同じ場所でできるので漏らすことが減ったのは大きなメリット。

class Sample
{
  // 全てのメンバが初期化できていることが一目瞭然
  int test{100};
  std::string test2{"hoge"};
  std::string test3{"hoge1"};
  std::string test4{"hoge2"};
  std::string test5{"hoge3"};
};

(3). 精度が落ちる初期化はコンパイルエラーにしてくれる

暗黙的な型変換により精度が落ちてしまう場合はコンパイラがコンパイルエラーにしてくれる。
そのため、コンパイル時に実装ミスに気付くことができる。
例えば以下のようにint型に対して浮動小数点型で初期化しようとすると、精度が落ちるのでコンパイルエラーにしてくれる。

int test {100.1}; // コンパイルエラー。整数型への変換で精度が落ちるため。

精度が落ちない場合は問題無いのでコンパイルは通してくれる。

double test {100}; // コンパイルOK。精度落ちないため。

コンパイル時に気づくことができるのでとても助かる。
ちなみに、変数を宣言と同時に初期化する際古いC++プログラマだと=を使うことがあると思うが、これは精度チェックはしてくれない。
ということで=初期化よりも{}の方がよい。

注意点

便利なuniform initializationだが気を付けなければいけないこともある。

(1). initializer_listのコンストラクタが優先されがち

initializer_listのコンストラクタが存在する型の場合、initializer_listのコンストラクタを使用する方が優先されがちになってしまう。
例えば以下のようにコンストラクタが定義されていたとしよう。

class Sample
{
public:
  Sample(int a, double b);
  Sample(std::initializer_list<int> list);
};

このSampleクラスに対して以下のように初期化しようとするとコンパイルエラーになってしまう。

// コンパイルエラー。initializer_listの方が優先され、暗黙的変換により精度が落ちるため。
Sample sample {1, 2.0};

このように意図したコンストラクタを上手く使用してくれないというケースが発生することがあるので注意。
initializer_listが定義されている型で通常のコンストラクタを使いたい場合は()でコンストラクタ呼び出しをする方が良いかもしれない。

(2). コンストラクタのメンバ初期化子が優先される

以下のようにメンバ変数の定義箇所とコンストラクタの初期化子リストで同じメンバを初期化してしまっている場合、コンパイルは通るがコンストラクタの初期化子リストの方の初期化が優先される。

sample.h
class Sample
{
public:
  Sample();
 int m_test{0};
  double m_test2{0.0};
};
sample.cpp
Sample::Sample() : m_test(1), m_test2{1.0}
{
}
main.cpp
int main()
{
  Sample sample;
  std::cout << sample.m_test << std::endl; // 1
  std::cout << sample.m_test2 << std::endl; // 1.0

  return 0;
}

あれ?定義時に{}初期化したはずなのに~とならないように、コンストラクタの方の初期化が実装されていないことを確認したうえで、メンバ変数定義箇所でuniform initializationを実装した方がよい。

参考

https://cpprefjp.github.io/lang/cpp11/uniform_initialization.html

Discussion