Skip to content

Latest commit

 

History

History
353 lines (263 loc) · 12.2 KB

initializer_lists.md

File metadata and controls

353 lines (263 loc) · 12.2 KB

初期化子リスト

  • cpp11[meta cpp]

概要

「初期化子リスト (initializer lists)」は、ユーザー定義型のオブジェクトに対して、波カッコによるリスト初期化を使用できるようにするようオーバーロードする機能である。

これによって、std::vectorのようなコンテナクラスに対しても、組み込み配列と同様に、波カッコによる簡易的な初期化構文を使用できる:

// 1, 2, 3の3要素を持つ配列オブジェクトを定義する
int ar[] = {1, 2, 3};
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 {1, 2, 3};

// 再代入
v2 = {4, 5, 6};

ユーザー定義型に対してリスト初期化を許可するためには、<initializer_list>ヘッダで定義されるstd::initializer_listクラスのオブジェクトをパラメータとして、コンストラクタや代入演算子をオーバーロードする:

#include <vector>
#include <initializer_list>

template <class T>
class MyVector {
  std::vector<T> data_;
public:
  // リスト初期化用のコンストラクタ
  MyVector(std::initializer_list<T> init)
    : data_(init.begin(), init.end()) {}
};

int main()
{
  MyVector<int> v = {1, 2, 3};
}
  • init.begin()[link /reference/initializer_list/initializer_list/begin.md]
  • init.end()[link /reference/initializer_list/initializer_list/end.md]

リスト初期化用のコンストラクタにexplicitを付けた場合、代入演算子によるリスト初期化は許可せず、代入演算子を伴わないリスト初期化のみが許可される:

#include <vector>
#include <initializer_list>

template <class T>
class MyVector {
  std::vector<T> data_;
public:
  // リスト初期化用のコンストラクタ
  explicit MyVector(std::initializer_list<T> init)
    : data_(init.begin(), init.end()) {}
};

int main()
{
// MyVector<int> v = {1, 2, 3}; // コンパイルエラー!
   MyVector<int> v {1, 2, 3};   // OK
}
  • init.begin()[link /reference/initializer_list/initializer_list/begin.md]
  • init.end()[link /reference/initializer_list/initializer_list/end.md]

仕様

  • 波カッコ { } を使用した初期化子のリストによるオブジェクトもしくは参照の初期化を、「リスト初期化 (list initialization)」と呼び、その初期化子を「初期化子リスト (initializer list)」と呼ぶ。初期化子リストは、カンマ区切りで要素を列挙する
  • 初期化子リストは、空であってもよい

初期化子リストは、以下の文脈で使用できる:

  • 変数定義での初期化子リストによる初期化

    struct X {
      X(std::initializer_list<int>) {}
    };
    
    X x1 {1, 2, 3};   // 直接初期化して変数定義
    X {1, 2, 3};      // 直接初期化して一時オブジェクトを定義
    X x2 = {1, 2, 3}; // コピー初期化して変数定義
  • new式での初期化子リストによる初期化

    new X {1, 2, 3}; // 動的記憶域でXオブジェクトを直接初期化
  • return

    X f()
    {
      return {1, 2, 3};
    }
  • 関数の引数

    void f(X) {}
    f({1, 2, 3});
  • 式の一部

    std::vector<X> xs {
      {1, 2, 3}, // 初期化子リストのなかでさらに初期化子リストを使用する
      {4, 5, 6}
    };
  • 基底クラスやメンバの初期化子

    struct Y : X {
      std::vector<int> values;
    
      Y()
        : X {1, 2, 3}, values {4, 5, 6} {}
    };
  • 代入演算子の右辺

    struct X {
      X& operator=(std::initializer_list<int>) { return *this; }
    };
    
    X x;
    x = {1, 2, 3};
  • 初期化子リストに縮小変換が要求された場合、プログラムは不適格となる

    struct X {
      X(std::initializer_list<int>) {}
    };
    
    X x1 = {1, 2, 3}; // OK
    //X x2 = {1, 2, 3.0}; // コンパイルエラー!3.0をint型に縮小変換できない
  • 縮小変換以外の型変換は許可される

    struct X {
      X(std::initializer_list<double>) {}
    };
    
    X x1 = {1, 2, 3};   // OK
    X x2 = {1, 2, 3.0}; // OK

以下の条件を満たすコンストラクタを、「初期化子リストコンストラクタ (initializer-list constructor)」と呼ぶ:

  • 任意の型Eを要素とするstd::initializer_list<E>型のパラメータをひとつだけとり、そのほかのパラメータを持たない
  • もしくは、std::initializer_list<E>型のパラメータおよび、それ以降にデフォルト引数を持つ
  • デフォルトコンストラクタと初期化子リストコンストラクタがある場合、空の初期化子リストが渡された際にはデフォルトコンストラクタが呼び出される

    #include <iostream>
    #include <initializer_list>
    
    struct X {
      X()
      {
        std::cout << "default constructor" << std::endl;
      }
    
      X(std::initializer_list<int>)
      {
        std::cout << "initializer-list constructor" << std::endl;
      }
    };
    
    int main()
    {
      X x = {}; // 「default constructor」が出力される
    }
  • 初期化子リストコンストラクタと、その初期化子リストの要素型と同じ型のパラメータリストを受け取るコンストラクタでは、初期化子リストコンストラクタが優先して呼び出される。そのような状況では、丸カッコでのコンストラクタ呼び出しが必要となる

    struct X {
      X(std::initializer_list<double>) {
        std::cout << 1 << std::endl;
      }
      X(double d) {
        std::cout << 2 << std::endl;
      }
    };
    
    X x1 = {3.0}; // 「1」が出力される
  • 異なる要素型を持つstd::initializer_list型同士でオーバーロードができる

    struct X {
      X(std::initializer_list<int>)
      {
        std::cout << 1 << std::endl;
      }
    
      X(std::initializer_list<double>)
      {
        std::cout << 2 << std::endl;
      }
    };
    
    X {1, 2, 3};       // 「1」が出力される
    X {1.0, 2.0, 3.0}; // 「2」が出力される
  • 初期化子リストが暗黙的にstd::initializer_list<E>に型変換される際、実装はE型の要素をN個持つ配列を確保するかのように振る舞う。変換されたstd::initializer_list<E>オブジェクトは、元となった初期化子リストの配列を参照する。以下のような初期化子リストの引数渡しがあった場合、

    struct X {
      X(std::initializer_list<double>) {}
    };
    
    X x = {1, 2, 3};

    実装は以下と等価の初期化を行う (実装が用意したstd::initializer_listクラスがポインタの組を受け取れると仮定する):

    double __a[3] = {double{1}, double{2}, double{3}};
    X x(std::initializer_list<double>(__a, __a+3));

    元となった配列の寿命は、変換先のstd::initializer_listオブジェクトと同じとなる

  • 初期化子リストをautoで受けた場合、std::initializer_list型に推論される。ただし、空の初期化子リストは推論に失敗する

    auto x1 = {1, 2, 3}; // x1の型はstd::initializer_list<int>
    //auto x2 = {};      // コンパイルエラー!x2の型を推論できない
  • 単一要素の初期化子リストをautoで受けた場合、C++11ではstd::initializer_list<T>型に推論されるが、C++17では直接初期化の場合T型に推論されるよう仕様が変更されるので注意

    auto x{1}; // C++17ではxの型はintになる
    auto x = {1}; // C++11,17共に、xの型はstd::initializer_list<int>。
  • 関数テンプレートのパラメータとして初期化子リストを受けとった場合は、std::initializer_list型には推論されない

    template <class T>
    void f(T) {}
    
    int main()
    {
      f({1, 2, 3}); // コンパイルエラー!Tの型を推論できない
    }
  • std::initializer_listの要素型のみを関数テンプレートで推論させることはできる

    template <class T>
    void f(std::initializer_list<T>) {}
    
    f({1, 2, 3}); // OK : Tはint
  • 初期化子リストに列挙した要素は、先頭から順番に評価されることが保証される

    #include <iostream>
    #include <initializer_list>
    
    int f()
    {
      std::cout << 1 << std::endl;
      return 1;
    }
    
    int g()
    {
      std::cout << 2 << std::endl;
      return 2;
    }
    
    int h()
    {
      std::cout << 3 << std::endl;
      return 3;
    }
    
    int main()
    {
      std::initializer_list<int> init = { f(), g(), h() }; // 1, 2, 3の順で出力される
    }

この機能が必要になった背景・経緯

C++の目標として、「組み込み型の振る舞いをユーザー定義型で定義できるようにする」というものがある。しかし、組み込み配列での波カッコを使用したリスト初期化は、ユーザー定義型に対してオーバーロードができなかった。それにより、std::vectorのようなコンテナクラスの初期化が使いにくいものとなっていた:

const int N = 3;
int ar[N] = {1, 2, 3};
std::vector<int> v(ar, ar + N);

この問題を解決するために、波カッコによるリスト初期化をユーザー定義型でオーバーロードする機能が求められ、std::initializer_listクラスとオーバーロード機能が導入された。

関連項目

参照