C++ Learning

第7回 統一初期化 { }

C++11 で導入された「統一初期化(ブレース初期化)」は、変数・配列・構造体・std::vector ・クラスのメンバまで、あらゆる場面を {} ひとつで書ける仕組みです。加えて、doubleint のような精度が落ちる変換(narrowing)をコンパイル時に検出してくれるので、C で書いていた初期化よりも安全です。

このページで押さえること
✅ 最低限ここだけ覚える
  • T x{値}; と書けば変数・構造体・vector まで何でも同じ
  • T x{};ゼロ初期化(未初期化状態を避ける)
  • narrowing 変換(例: doubleint)をコンパイル時に弾いてくれる
  • 迷ったら {} を選ぶのがモダン C++ の流儀
⭐ 余裕があれば読む
  • MyClass obj(); と書くと関数宣言に化ける(most vexing parse)
  • std::vector<int> v(3,5);v{3,5};決定的な違い
  • () / = / {} の使い分けの現代基準

1. これまでの初期化はバラバラだった

C や C++03 までは、何を初期化するかで書き方が違っていました。覚えることが多く、場所によっては使えない書き方もありました。

C: 場面ごとに違う書き方C
// 変数は = で初期化 int x = 5; // 配列は {} で int a[3] = {1, 2, 3}; // 構造体も {} で(C99 以降) struct P { int x, y; }; struct P p = {1, 2}; // malloc した領域は別途 memset int* buf = malloc(10*sizeof(int)); memset(buf, 0, 10*sizeof(int));
書き方が3〜4 通りに分かれる
C++11: { } に統一C++11〜
// 何でも {} int x{5}; int a[3]{1, 2, 3}; struct P { int x, y; }; P p{1, 2}; std::vector<int> v{1, 2, 3, 4}; // ゼロ埋めも標準構文で int zero[10]{};
全部{} 一択で OK
なぜ統一が必要だったか: 初期化の書き方が場所ごとに違うと、「ここは = が必要? () は使える?」と毎回迷います。統一初期化はこの混乱を解消し、どこでも同じ構文で書けるようにした機能です。

2. 統一初期化 { } の基本

基本形はシンプルです。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}; のように = を挟む必要はありません(挟んでも同じ意味の「コピーリスト初期化」になります)。

3. narrowing を弾く安全性

統一初期化のもう一つの大きな利点は、精度が落ちる変換(narrowing)をコンパイル時にエラーにしてくれることです。C や () 初期化では通ってしまうバグを防げます。

() / = だと通ってしまう危険
int a = 3.7; // a = 3(切り捨て、警告のみ) int b(3.7); // b = 3(同じく通る) long big = 10000000000L; int c = big; // 警告のみ、ビット落ち
数値が静かに壊れる可能性
{} ならエラー安全
int d{3.7}; // ❌ コンパイルエラー: narrowing int e = {3.7}; // ❌ 同じくエラー long big = 10000000000L; int f{big}; // ❌ 範囲外でエラー // 意図して切り捨てるなら static_cast を明示 int g{static_cast<int>(3.7)}; // OK
バグをコンパイル時に弾ける

検出される「narrowing」は、doubleint / longint / unsignedsigned(値が入らない場合)など、情報が失われる方向の暗黙変換です。意図的に切り捨てたい場合は static_cast で明示すれば通ります。

これは実用的に大きい: 「何となく書いたら通った」が減り、型の取り違えや範囲外代入がコンパイル時に気付けます。C で printf("%d", 3.14) のような型違いで苦しんだ経験がある人ほどメリットを感じます。

4. ゼロ初期化 T x{}

中身が空の {} を書くと、型に応じた「ゼロ相当の値」で初期化されます。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
未初期化のままにしない: C で int x; と書くと x不定値(ランダムな値)になり、使うと未定義動作です。C++ でも同じ挙動なので、int x{}; と書く習慣をつけるだけで「未初期化変数を使って落ちる」系のバグを構造的に防げます。
メンバ変数の既定値: クラスのメンバも int count{0}; のように宣言時にゼロ初期化しておくと、コンストラクタを書き忘れてもゴミ値にならず、安心して使えます。本サイトのクラス例ではこれを標準にします。
余裕があれば読む ― ここから先は応用
最低限は上の 4 節で完結。ここからは {}() と違う動きをする微妙な場面の話なので、必要になったときに戻ってくれば OK。

5. most vexing parse の回避

C++ には歴史的に most vexing parse(極めて厄介な構文解析)という罠があります。「オブジェクトを作ったつもりが、コンパイラには関数宣言として解釈される」問題です。

() だと関数宣言に化ける
class Timer { public: Timer(); void start(); }; int main() { Timer t(); // ❌ オブジェクト生成ではない! // 「引数なし、戻り値 Timer の関数 t の宣言」 t.start(); // ❌ エラー: t は関数 }
関数宣言と解釈される
{} なら曖昧さなし安全
class Timer { public: Timer(); void start(); }; int main() { Timer t{}; // ✅ オブジェクト生成 t.start(); // OK }
明確にオブジェクト生成

原因は「Timer()」の () が空引数リストと区別できないこと。C++ の文法は曖昧な場合「宣言として解釈できるなら宣言優先」というルールがあるため、意図と違う方に倒れます。{} は宣言構文として解釈される余地がないので、必ずオブジェクト生成になります。

6. () と {} の使い分けの罠

「何でも {}」と言いましたが、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 種類のコンストラクタが用意されているためです。

{} を使うと initializer_list 版が優先的に呼ばれるため、「35 を並べた 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 問で確認してみましょう。

Q1. 次のうちコンパイルエラーになるものはどれ?

int x{42};
int x = 3.7;(警告のみ)
int x{3.7};
int x = 0;
{} 初期化では doubleint のような narrowing 変換がコンパイルエラーになります。int x = 3.7;int x(3.7); は警告で済んで通ってしまう(値 3 に切り捨て)点に注意。意図的に切り捨てたいなら static_cast<int>(3.7) を明示します。

Q2. int x{}; の値は?

未定義(不定値)
1
0
コンパイルエラー
空の {} はゼロ初期化を意味し、int なら 0、double なら 0.0、ポインタなら nullptrstd::string なら空文字列になります。int x; とすると不定値になるので、「宣言時に必ず {} を付ける」のが安全な習慣です。

Q3. std::vector<int> v{3, 5}; の要素は?

{5, 5, 5} ― 要素 3 個、全部 5
{3, 5} ― 要素 2 個、値は 3 と 5
要素数 3 で初期値 0、0、0
コンパイルエラー
{}std::initializer_list 版コンストラクタを優先的に呼ぶため、35 を並べた要素 2 個の vector になります。「要素数 3 個で全部 5」としたい場合は std::vector<int> v(3, 5);() で呼ぶ必要があります。
この記事をシェア