リスト初期化とオーバーロード解決

この記事では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):
    • スカラー型…整数リテラル0を型変換した値で初期化
    • クラス…サブオブジェクトをゼロ初期化し、パディングを0で埋める
    • 共用体…先頭の非静的データメンバをゼロ初期化し、パディングを0で埋める
    • 配列…各要素をゼロ初期化
    • リファレンス…何もしない
  • デフォルト初期化(default-initialization、8.5p7):
    • クラス…デフォルトコンストラクタが呼ばれる
    • 配列…各要素をデフォルト初期化
    • それ以外…何もしない
  • 値初期化(value-initialization、8.5p8):
    • デフォルトコンストラクタを持たないか、又はこれがユーザ定義若しくはdeleted指定されたクラス…オブジェクトはデフォルト初期化
    • ユーザ定義又はdeleted指定されたデフォルトコンストラクタを持たないクラス…オブジェクトはゼロ初期化され、非自明なデフォルトコンストラクタがあれば、さらにデフォルト初期化
    • 配列…各要素を値初期化
    • それ以外…オブジェクトはゼロ初期化
  • コピーによる初期化(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は、ヘッダで定義されており(18.9p1)、const E型の配列へのアクセスを提供する(18.9p2)。

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):

  1. まず、初期化リスト全体を単一の実引数とみなし、Tの初期化リストコンストラクタを候補関数とする。
  2. 最適な(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

余談

C++11で導入されたdeleted functionの機能は、関数定義を削除するのであって、関数宣言は残ってオーバーロード解決に寄与する。上記にあるオーバーロード解決のサンプルコードについて、deleted指定で頑張っていたけど、うまくいかなかった。

  1. 本の虫: deleted definitionによるクラスの初期化の制御
  2. 本の虫: deleted定義