リスト初期化とオーバーロード解決
この記事ではC++11で追加されたリスト初期化について、今更ながらまとめてみた。ソースはN3690。
uniform initialization、list initialization
これまでは初期化という同一のセマンティクスを実現するのに、ばらばらで不統一なシンタックスを使っていた。
int i = 2013; char s[] = { 'A', 'p', 'p', 'l', 'e' }; struct X { int i; char s[ 4 ]; } x = { 16, { 'a', 'b', 'c', '\0' } }; std::vector< double > v( 8, 1.0 );
C++11で提案されたuniform initializationは、変数宣言など(後述)で波カッコリスト{…}を用いる統一したシンタックスで、初期化リスト{…}による初期化のセマンティクスを実現するもの。これにより、型に依存しないシンタックスが必然的に求められるテンプレートにおいて、初期化の記述性を向上させることができる。
int i = { 2013 }; char s[] = { 'A', 'p', 'p', 'l', 'e' }; struct X { int i; char s[ 4 ]; } x = { 16, { 'a', 'b', 'c', '\0' } }; std::vector< double > v{ 8, 1.0 };
初期化リスト{…}による初期化、といっても「初期化される型」と「初期化リストの内容」に応じて具体的な振る舞いが異なるため、その詳細について調べてみた。なお、初期化リストの要素にはリテラル・変数・関数を含む任意の式を使用でき、変数のstorage durationによらない(8.5p2)。
初期化にまつわる用語
- ゼロ初期化(zero-initialization、8.5p6):
- デフォルト初期化(default-initialization、8.5p7):
- クラス…デフォルトコンストラクタが呼ばれる
- 配列…各要素をデフォルト初期化
- それ以外…何もしない
- 値初期化(value-initialization、8.5p8):
- コピーによる初期化(copy-initialization、8.5p15):
- T x = a; の形式(代入形式)
- 関数への実引数渡し
- 関数からの返却値
- 例外のスロー
- 例外のキャッチ
- 集約メンバの初期化
- 直接的な初期化(direct-initialization、8.5p16):
- T x(a); の形式(関数形式)
- T x{a}; の形式(波カッコ形式)
- new式
- static_cast式
- 関数表記の型変換
- 基底・メンバ初期化子
std::initializer_listと初期化リストコンストラクタ
std::initializer_listは、ヘッダ
namespace std { template<class E> class initializer_list { public: typedef E value_type; typedef const E& reference; typedef const E& const_reference; typedef size_t size_type; typedef const E* iterator; typedef const E* const_iterator; constexpr initializer_list() noexcept; constexpr size_t size() const noexcept; // number of elements constexpr const E* begin() const noexcept; // first element constexpr const E* end() const noexcept; // one past the last element }; // 18.9.3 initializer list range access template<class E> constexpr const E* begin(initializer_list<E> il) noexcept; template<class E> constexpr const E* end(initializer_list<E> il) noexcept; }
初期化リストコンストラクタ(initializer-list constructor)とは、第一引数がstd::initializer_list< E >(又はそのリファレンス)であり、その他の仮引数がないか、すべてデフォルト実引数が設定されているコンストラクタのこと(8.5.4p2)。
これが呼び出されると、初期化リストの内容・サイズでconst E型の固定長配列が作られて、その配列を参照するようにstd::initializer_list< E >が構築される(8.5.4p5)。作られる配列は一時オブジェクト扱いだが、std::initializer_list< E >をこれで初期化することで、配列の寿命(lifetime)はリファレンスにバインドされた時とまったく同じになる(8.5.4p6)。
集約
集約(aggregate)とは、以下のこと(8.5.1p1)であり、C言語で配列や構造体の初期化に使われた波カッコ初期化の文法が、ここからC++に残った。
- 配列、又は
- ユーザ定義のコンストラクタ、private又はprotectedな非静的データメンバ、基底クラス、仮想関数を一切含まないクラス
集約を初期化リストで初期化する場合、集約のメンバが添字の増加順又はメンバの宣言順に、初期化リストの要素によって(コピーによる)初期化される(8.5.1p2)。初期化リストに更に初期化リストが含まれている場合は、対応するメンバが集約であれば、これに対して再帰的にこのルールを適用する(8.5.1p2)。
サイズ不明の配列を初期化リストで初期化する場合、配列のサイズは初期化リストのそれになる。空リストによる初期化はできない(8.5.1p3)。静的データメンバ及び匿名ビットフィールドは、集約の初期化では無視される(8.5.1p4)。
リスト初期化
リスト初期化は、直接的な初期化・コピーによる初期化のどちらのコンテキストでも可能(8.5.4p1)で、次の場面で使うことができる:
- 変数定義における初期化子として
- new式における初期化子として
- return文において
- range-based for文において…for ( auto i : ここ )
- 関数に渡す実引数として
- 添字として
- コンストラクタ呼び出しへ渡す実引数として
- 非静的データメンバの初期化子として
- コンストラクタ内のメンバ初期化子の中で
- 代入文の右辺で
型Tのオブジェクト又はリファレンスのリスト初期化は以下の手順で行われる(8.5.4p3):
- Tが集約である場合、上記のとおり集約の初期化が行われる
- 空リストによる初期化で、かつTがデフォルトコンストラクタを持つクラスの場合、オブジェクトは値初期化される(後述)
- Tがstd::initializer_list< E >の特殊化である場合、initializer_listのprvalueが生成され、オブジェクトの初期化に使われる
- Tがクラスの場合、適用可能なコンストラクタの中からオーバーロード解決を通して最適なものが選ばれる(後述)
- 初期化リストに要素が1つだけある場合、オブジェクト又はリファレンスはその要素で初期化される
- Tが参照型の場合、Tが参照している型のprvalueな一時オブジェクトが生成・初期化され、リファレンスはその一時オブジェクトにバインドされる
- 空リストによる初期化の場合、オブジェクトは値初期化される
- これらの条件を上から順に適用していき、どれにも該当しない場合、プログラムはill-formedである
オーバーロード解決
集約でないクラスTをリスト初期化すると、呼び出すコンストラクタを選択するため、オーバーロード解決が2段階で行われる(13.3.1.7p1):
- まず、初期化リスト全体を単一の実引数とみなし、Tの初期化リストコンストラクタを候補関数とする。
- 最適な(viable)初期化リストコンストラクタが見つからなかった場合、初期化リストの各要素を実引数とみなし、Tの全てのコンストラクタを候補関数として再度オーバーロード解決を試みる。
※ただし、空リストによる初期化で、かつTがデフォルトコンストラクタを持っている場合、①は省略される。
リスト初期化で呼び出されるコンストラクタについて、以下のサンプルコードで実験してみる:
#include <iostream> #include <initializer_list> #include <string> class A { public: A() { std::cout << "A::A()" << std::endl; } A( std::initializer_list< int > ) { std::cout << "A::A( initializer_list< int > )" << std::endl; } A( std::initializer_list< std::string > ) { std::cout << "A::A( initializer_list< string > )" << std::endl; } A( int ) { std::cout << "A::A( int )" << std::endl; } A( int, std::string ) { std::cout << "A::A( int, string )" << std::endl; } A( double ) { std::cout << "A::A( double )" << std::endl; } A( double, std::string ) { std::cout << "A::A( double, string )" << std::endl; } }; class B { public: B() { std::cout << "B::B()" << std::endl; } B( std::initializer_list< int > ) { std::cout << "B::B( initializer_list< int > )" << std::endl; } // NOT declared // B( std::initializer_list< std::string > ); B( int ) { std::cout << "B::B( int )" << std::endl; } B( int, std::string ) { std::cout << "B::B( int, string )" << std::endl; } B( double ) { std::cout << "B::B( double )" << std::endl; } B( double, std::string ) { std::cout << "B::B( double, string )" << std::endl; } }; class C { public: // NOT declared // C(); C( std::initializer_list< int > ) { std::cout << "C::C( initializer_list< int > )" << std::endl; } C( std::initializer_list< std::string > ) { std::cout << "C::C( initializer_list< string > )" << std::endl; } C( int ) { std::cout << "C::C( int )" << std::endl; } C( int, std::string ) { std::cout << "C::C( int, string )" << std::endl; } C( double ) { std::cout << "C::C( double )" << std::endl; } C( double, std::string ) { std::cout << "C::C( double, string )" << std::endl; } }; class D { public: // NOT declared // D(); D( std::initializer_list< int > ) { std::cout << "D::D( initializer_list< int > )" << std::endl; } // NOT declared // D( std::initializer_list< std::string > ); D( int ) { std::cout << "D::D( int )" << std::endl; } D( int, std::string ) { std::cout << "D::D( int, string )" << std::endl; } D( double ) { std::cout << "D::D( double )" << std::endl; } D( double, std::string ) { std::cout << "D::D( double, string )" << std::endl; } }; int main() { A a0 {}; A a1 { 1, 2, 3 }; A a2 { "hoge", "piyo", "fuga" }; A a3 { 100 }; // A( initializer_list< int > ) A a4 { 100, "hello" }; A a5 { 3.14 }; // A( initializer_list< int > ) warning: narrowing: (double) -> (int) A a6 { 3.14, "hello" }; std::cout << "---" << std::endl; B b0 {}; B b1 { 1, 2, 3 }; // B b2 { "hoge", "piyo", "fuga" }; // error: invalid conversion: (const char*) -> (int) B b3 { 100 }; B b4 { 100, "hello" }; B b5 { 3.14 }; B b6 { 3.14, "hello" }; std::cout << "---" << std::endl; // C c0 {}; // error: ambiguous overload resolution: C( initializer_list< int > ) or C( initializer_list< string > ) C c1 { 1, 2, 3 }; C c2 { "hoge", "piyo", "fuga" }; C c3 { 100 }; C c4 { 100, "hello" }; C c5 { 3.14 }; C c6 { 3.14, "hello" }; std::cout << "---" << std::endl; D d0 {}; // D( initializer_list< int > ) D d1 { 1, 2, 3 }; // D d2 { "hoge", "piyo", "fuga" }; D d3 { 100 }; D d4 { 100, "hello" }; D d5 { 3.14 }; D d6 { 3.14, "hello" }; return 0; }
実行結果は以下のとおり(GCC4.8.1)で、d0でD( initializer_list< int > )が呼ばれており、空リスト初期化は必ずしもデフォルトコンストラクタ呼び出しにならない。また、c0のように、初期化リストコンストラクタがinitializer_list< T >の異なる特殊化でオーバーロードされていたら、空リストに対してオーバーロード解決できない。
なお、リスト初期化にもかかわらずa5、b5、c5、d5でdouble→intのナローイングが起こっているのに、なぜかエラーではなく警告が出た。GCCの仕様なのか、よく分からない…
A::A() A::A( initializer_list< int > ) A::A( initializer_list< string > ) A::A( initializer_list< int > ) A::A( int, string ) A::A( initializer_list< int > ) A::A( double, string ) --- B::B() B::B( initializer_list< int > ) B::B( initializer_list< int > ) B::B( int, string ) B::B( initializer_list< int > ) B::B( double, string ) --- C::C( initializer_list< int > ) C::C( initializer_list< string > ) C::C( initializer_list< int > ) C::C( int, string ) C::C( initializer_list< int > ) C::C( double, string ) --- D::D( initializer_list< int > ) D::D( initializer_list< int > ) D::D( initializer_list< int > ) D::D( int, string ) D::D( initializer_list< int > ) D::D( double, string )
ナローイング
ナローイング(narrowing conversion)とは以下の暗黙の型変換(8.5.4p7)を指す:
- 浮動小数点型→整数型
- long double→double or float、double→float(変換元が定数で、その値が変換後の型で表現できる場合を除く)
- 整数型 or unscoped列挙型→浮動小数点型(変換元が定数で、その値が変換後の型にぴったりはまり、元の型に戻すとオリジナルの値を再現する場合を除く)
- 整数型 or unscoped列挙型→表現できる範囲のより狭い整数型(変換元が定数で、整数昇格後の値が変換後の型にぴったりはまる場合を除く)
リスト初期化ではナローイングは禁止(8.5.1p2、8.5.4.p3、8.5.4p5)されている。C++03までは集約の初期化でナローイングが認められていたが、C++11でこの後方互換性を破棄した。以下はN3690からの引用:
int x = 999; // x is not a constant expression const int y = 999; const int z = 99; char c1 = x; // OK, though it might narrow (in this case, it does narrow) char c2{x}; // error: might narrow char c3{y}; // error: narrows (assuming char is 8 bits) char c4{z}; // OK: no narrowing needed unsigned char uc1 = {5}; // OK: no narrowing needed unsigned char uc2 = {-1}; // error: narrows unsigned int ui1 = {-1}; // error: narrows signed int si1 = { (unsigned int)-1 }; // error: narrows int ii = {2.0}; // error: narrows float f1 { x }; // error: might narrow float f2 { 7 }; // OK: 7 can be exactly represented as a float int f(int); int a[] = { 2, f(2), f(2.0) }; // OK: the double-to-int conversion is not at the top level