C++11 で導入された「統一初期化(ブレース初期化)」は、変数・配列・構造体・std::vector ・クラスのメンバまで、あらゆる場面を {} ひとつで書ける仕組みです。加えて、double → int のような精度が落ちる変換(narrowing)をコンパイル時に検出してくれるので、C で書いていた初期化よりも安全です。
T x{値}; と書けば変数・構造体・vector まで何でも同じT x{}; でゼロ初期化(未初期化状態を避ける)double → int)をコンパイル時に弾いてくれる{} を選ぶのがモダン C++ の流儀MyClass obj(); と書くと関数宣言に化ける(most vexing parse)std::vector<int> v(3,5); と v{3,5}; の決定的な違い() / = / {} の使い分けの現代基準C や C++03 までは、何を初期化するかで書き方が違っていました。覚えることが多く、場所によっては使えない書き方もありました。
= が必要? () は使える?」と毎回迷います。統一初期化はこの混乱を解消し、どこでも同じ構文で書けるようにした機能です。
基本形はシンプルです。T x{値}; と書くだけで、ほぼ全ての場面をカバーできます。
// 単純な型 int x{42}; double d{3.14}; char c{'A'}; // 配列 int a[]{1, 2, 3}; // std::array / std::vector std::array<int, 3> arr{1, 2, 3}; std::vector<int> v{1, 2, 3, 4, 5}; // クラス / 構造体 struct Point { int x, y; }; Point p{10, 20}; // 関数の戻り値でも使える Point make_origin() { return {0, 0}; } // メンバ初期化 class Box { int w{0}, h{0}; // メンバの既定値 };
= は不要: int x{42}; と書けばよく、int x = {42}; のように = を挟む必要はありません(挟んでも同じ意味の「コピーリスト初期化」になります)。
統一初期化のもう一つの大きな利点は、精度が落ちる変換(narrowing)をコンパイル時にエラーにしてくれることです。C や () 初期化では通ってしまうバグを防げます。
検出される「narrowing」は、double → int / long → int / unsigned → signed(値が入らない場合)など、情報が失われる方向の暗黙変換です。意図的に切り捨てたい場合は static_cast で明示すれば通ります。
printf("%d", 3.14) のような型違いで苦しんだ経験がある人ほどメリットを感じます。
中身が空の {} を書くと、型に応じた「ゼロ相当の値」で初期化されます。C の int x = 0; / memset(arr, 0, sizeof(arr)); に相当する書き方を、1 つの構文で統一できます。
int x{}; // 0 double d{}; // 0.0 bool b{}; // false char* p{}; // nullptr std::string s{}; // 空文字列 "" std::vector<int> v{}; // 空の vector int arr[10]{}; // すべて 0 struct Point { int x, y; }; Point pt{}; // x=0, y=0
int x; と書くと x は不定値(ランダムな値)になり、使うと未定義動作です。C++ でも同じ挙動なので、int x{}; と書く習慣をつけるだけで「未初期化変数を使って落ちる」系のバグを構造的に防げます。
int count{0}; のように宣言時にゼロ初期化しておくと、コンストラクタを書き忘れてもゴミ値にならず、安心して使えます。本サイトのクラス例ではこれを標準にします。
{} が () と違う動きをする微妙な場面の話なので、必要になったときに戻ってくれば OK。C++ には歴史的に most vexing parse(極めて厄介な構文解析)という罠があります。「オブジェクトを作ったつもりが、コンパイラには関数宣言として解釈される」問題です。
原因は「Timer()」の () が空引数リストと区別できないこと。C++ の文法は曖昧な場合「宣言として解釈できるなら宣言優先」というルールがあるため、意図と違う方に倒れます。{} は宣言構文として解釈される余地がないので、必ずオブジェクト生成になります。
「何でも {}」と言いましたが、1 箇所だけ例外があります。std::vector のコンストラクタでは、() と {} で意味が変わります。
std::vector<int> v1(3, 5); // → {5, 5, 5} 要素 3 個、全部 5 std::vector<int> v2{3, 5}; // → {3, 5} 要素 2 個
理由は、std::vector には 2 種類のコンストラクタが用意されているためです。
vector(size_t count, const T& value) ― 要素数と初期値を受け取るvector(std::initializer_list<T>) ― 並んだ値を受け取る({} はこちらを優先){} を使うと initializer_list 版が優先的に呼ばれるため、「3 と 5 を並べた vector」になります。要素数を指定して作りたい場合は () を使うか、C++11 の std::vector<int> v(n, value); のように意図的に分けます。
{}:安全性と narrowing 検出のため():std::vector<int> v(100, 0);= 初期化(int x = 5;)も引き続き OK。読み手に慣れている記法統一初期化は「C++ の変数をどう作るか」の土台になります。次章(第 8 回)は同じ変数・ポインタまわりのモダン C++ 機能として、nullptr(NULL の代替)と constexpr(コンパイル時計算)を扱います。
ここまでの理解を 3 問で確認してみましょう。
int x{42};int x = 3.7;(警告のみ)int x{3.7};int x = 0;{} 初期化では double → int のような narrowing 変換がコンパイルエラーになります。int x = 3.7; や int x(3.7); は警告で済んで通ってしまう(値 3 に切り捨て)点に注意。意図的に切り捨てたいなら static_cast<int>(3.7) を明示します。int x{}; の値は?{} はゼロ初期化を意味し、int なら 0、double なら 0.0、ポインタなら nullptr、std::string なら空文字列になります。int x; とすると不定値になるので、「宣言時に必ず {} を付ける」のが安全な習慣です。std::vector<int> v{3, 5}; の要素は?{} は std::initializer_list 版コンストラクタを優先的に呼ぶため、3 と 5 を並べた要素 2 個の vector になります。「要素数 3 個で全部 5」としたい場合は std::vector<int> v(3, 5); と () で呼ぶ必要があります。