Flat Leon Works

アプリやゲームを作ってます。

【C++】初期化子リスト関連機能を大雑把に理解する

C++11から初期化子リストが導入されました。初期化子リストは簡単に見えて結構難しい機能です。そこで初期化子リストを大雑把に理解するための記事を書いてみることにしました。理解することを優先しているので言葉の定義などが不正確だったりするかもしれません。というか筆者もC++11を勉強中の身なので大嘘書いている可能性もあります。間違っていたらごめんなさい。

はじめに

初期化子リスト自体は「std::initializer_list<T>型のオブジェクトを楽に構築するための機能」です。そこから発展したものとして、「リスト初期化」「統一初期化」という機能が存在します。この記事では、「初期化子リスト機能」と「初期化子リスト全般に関連する機能」を区別するために、前者を「初期化子リスト」、後者を「初期化子リスト関連機能」と呼ぶことにします。つまり、今後この記事内では単に「初期化子リスト」と記述した場合は、「std::initializer_list<T>型のオブジェクトを楽に構築するための機能」を指します。

さて、この「初期化子リスト関連機能」が難しいのは3つの機能が1つの機能のように見えてしまっていることです。3つの機能とは「初期化子リスト」「リスト初期化」「統一初期化」のことです。これらの3つの機能は別々の機能ですが関連した機能でもあります。そこで、この記事ではこれらの3つの機能を順番に説明していきます。そして「初期化子リスト関連機能」をさらに難しくしているいくつかの特殊な仕様もあるのでそれも紹介します。

というわけで、この記事は以下のような構成になります。

  • 「初期化子リスト」の解説
  • 「リスト初期化」の解説
  • 「統一初期化」の解説
  • 初期化子リスト関連機能の注意すべき仕様

「初期化子リスト」 〜 std::initializer_list<T>型オブジェクトを楽に構築 〜

「初期化子リスト」をとても強引に説明すると、{式, 式, 式...}という記法でstd::initializer_list<T>型のオブジェクトを作れる機能です。これが「初期化子リスト関連機能」の基本中の基本です。

#include <stdio.h>
#include <initializer_list> // std::initializer_listを利用するのに必要
void func( std::initializer_list<int> list )
{
    for ( int value : list )
    {
        printf( "%d\n", value );
    }
}
int main()
{
    func( {1, 2, 8} ); // そのまま渡す
    std::initializer_list<int> list = {4,5,6}; // 一旦変数に入れてから
    func( list ); // 渡す
    return 0;
}

std::initializer_list<int>という型を見てわかるとおり、{式, 式, 式...}のそれぞれの式の型は同じである必要があります。

「リスト初期化」 〜 オブジェクト構築時の()を省略 〜

「初期化子リスト」はコンストラクタでも使うことができます。std::initializer_list<int>を1つだけ引数に取るコンストラクタを「初期化子リストコンストラクタ」と呼びます。初期化子リストコンストラクタでオブジェクトを構築する場合は()を省略して記述することができます。さらに、クラス名 識別子 = クラス名();形式の変数定義の場合は右辺のクラス名も省略することができます。

このように()やクラス名を省略した初期化子リストコンストラクタ呼び出しを「リスト初期化(list initialization)」と呼びます。*1

#include <stdio.h>
#include <initializer_list>

class A
{
public:
    A( std::initializer_list<int> list )
    {
        for ( int value : list )
        {
            printf( "%d\n", value );
        }    
    }
};

int main()
{
    // 「クラス名 識別子();」形式の変数定義
    {
        A a( {1, 2, 8} ); // 普通にオブジェクト構築
        A a2{1, 2, 8}; // ()を省略
    }
    // 「クラス名 識別子 = クラス名();」形式の変数定義
    {
        A a = A( {1, 2, 8} ); // 普通にオブジェクト構築
        A a2 = A{1, 2, 8}; // ()を省略
        A a3 = {1, 2, 8}; // A()を省略
    }
    return 0;
}

「統一初期化」 〜 通常のコンストラクタでもリスト初期化記法 〜

「統一初期化」あるいは「一様初期化」は、先程の「リスト初期化」を初期化子リストコンストラクタでない通常のコンストラクタの呼び出しでも使えるようにした機能です。つまり、通常のコンストラクタ呼び出し時の(式,式,式...)ではなく{式,式,式...}形式で引数を与えることができます。

#include <stdio.h>

class B
{
public:
    B( int v0, int v1, bool flag )
    {
    }
};

int main()
{
    // 「クラス名 識別子();」形式の変数定義
    {
        B b( 1, 2, false ); // 普通にオブジェクト構築
        B b2{ 1, 2, false }; // リスト初期化と同じ形式でオブジェクト構築
    }
    // 「クラス名 識別子 = クラス名();」形式の変数定義
    {
        B b = B( 1, 2, false ); // 普通にオブジェクト構築
        B b2 = B{ 1, 2, false }; // リスト初期化と同じ形式でオブジェクト構築
        B b3 = { 1, 2, false }; // リスト初期化と同じ形式でオブジェクト構築
    }
    return 0;
}

上記Bクラスのコンストラクタの引数はstd::initializer_list<T>になっていないのに、{式,式,式...}によるオブジェクト構築ができています。これが「統一初期化」の機能です。なお、「統一初期化」で利用する{式,式,式...}はstd::initializer_list型でないので、中身の型がバラバラでも記述可能です。

変数定義以外での「リスト初期化」「統一初期化」

ここまで「リスト初期化」「統一初期化」のコード例では変数定義のみしか扱っていませんでした。しかし、初期化、つまりオブジェクト構築の場面は変数定義以外にもたくさんあります。以下はC++でのオブジェクト構築が発生する場面一覧です。

  • 変数/配列の定義時のオブジェクト構築
  • 一時オブジェクトの構築
  • newによるオブジェクト構築
  • 派生クラスコンストラクタから基底クラスを構築
  • 関数の引数の値渡し時(コピーによるオブジェクト構築)
  • 関数の戻り値を返すとき(コピーによるオブジェクト構築)
  • 例外を投げるとき(コピーによるオブジェクト構築)
  • 例外をキャッチするとき(コピーによるオブジェクト構築)

これらのオブジェクト構築の場面でも、「リスト初期化」「統一初期化」を使うことができます。基本的には()またはクラス名()を省略して{}による記述が使えると考えておけばOKです。

初期化子リストの注意すべき仕様

上記、「初期化子リスト」「リスト初期化」「統一初期化」が初期化子リストの基本です。初期化子リストにはこれ以外にも注意すべき仕様がいくつかあります。

{}を使ってもstd::initializer_list<T>にならない場合がある

{}は基本的にはstd::initializer_list<T>型のオブジェクトになりますが、それ以外に解釈される場合もあります。

  • 空の{}を使いリスト初期化を行った場合 -> デフォルトコンストラクタが呼ばれる
  • 統一初期化の場合 -> {}の中身が通常の引数として解釈される
#include <stdio.h>
#include <initializer_list>

class A
{
public:
    A(){ printf( "A()\n" ); }
    A(int,const char*){ printf( "A(int,const char*)\n" ); }
    A(std::initializer_list<int>){ printf( "A(std::initializer_list<int>)\n" ); }
};

int main() {
    A a0{};        // 出力:A() // 空の{}でリスト初期化を行っているので、std::initializer_list<T>にならない
    A a1{1,"abc"}; // 出力:A(int,const char*) // 統一初期化なので、std::initializer_list<T>にならない
    A a2{1,2};     // 出力:A(std::initializer_list<int>) // リスト初期化だが空の{}ではないのでstd::initializer_list<T>になる
    A a3({});      // 出力:A(std::initializer_list<int>) // 空の{}だがリスト初期化ではないので、std::initializer_list<T>になる
    return 0;
}

「初期化子リスト」は式ではない

「初期化子リスト」は実は式ではありません。式を記述する場所ではたいてい「初期化子リスト」も記述できるので、式のように見えますが式ではありません。 なので、「初期化子リスト」はdecltype()では使えなかったりします。

「リスト初期化」と「統一初期化」でのオーバーロード

「リスト初期化」と「統一初期化」は見た目上同じ記法になるため、初期化子リストコンストラクタと通常のコンストラクタのどちらの呼び出しとも解釈できる場合があります。このような場合、初期化子リストコンストラクタが優先して呼び出されます。通常のコンストラクタの方を呼び出したい場合、「統一初期化」記法は諦めて{}ではなく()を使う必要があります。

#include <stdio.h>
#include <initializer_list>

class A
{
public:
    A(){ printf( "A()\n" ); }
    A(int,int){ printf( "A(int,int)\n" ); }
    A(std::initializer_list<int>){ printf( "A(std::initializer_list<int>)\n" ); }
};

int main() {
    A a1{1,2};     // 出力:A(std::initializer_list<int>) // 統一初期化だと解釈するとA(int,int)になるが、リスト初期化が優先される
    A a2(1,2);    // 出力:A(int,int) // {}をやめれば、当然通常のコンストラクタであるA(int,int)と解釈される
    return 0;
}

型推論

  • 空の「初期化子リスト」は型推論させることはできない
  • 素数が1の「初期化子リスト」はC++17以降ではstd::initializer_list<T>ではなくT型に推論される
  • テンプレート引数を明示せずに関数テンプレートの引数として「初期化子リスト」を渡すことはできない
    • ただし、std::initializer_list<T>形式にすれば可能
#include <stdio.h>
#include <initializer_list>
 
template<class T> void func1( T value ){}
template<class T> void func2( std::initializer_list<T> value ){}
 
int main() {
    // auto v1 = {}; // コンパイルエラー: 空の初期化子リストは型推論させることはできない
    auto v2 = {1,2,3}; // 空でない場合は型推論が働く
    auto v3 = {1}; // C++14まではstd::initializer_list<int>と推論される。C++17ではintと推論される
    // func1( {1,2,3} ); // コンパイルエラー: テンプレート引数を明示せずに関数テンプレートの引数として初期化子リストを渡すことはできない
    func1<std::initializer_list<int>>( {1,2,3} ); // テンプレート引数を明示すればOK
    func2( {1,2,3} ); // std::initializer_list<T>ならOK
    return 0;
}

まとめ

  • 「初期化子リスト関連機能」は大きく3つの機能にわけられる
  • 「初期化子リスト」はstd::initializer_list<T>型オブジェクトを楽に構築するための機能
  • 「リスト初期化」は「初期化子リスト」でオブジェクトを構築する際の()あるいはクラス名()を省略できる機能
  • 「統一初期化」は通常のコンストラクタでも「リスト初期化」記法を使えるようにした機能
  • いくつか注意すべき仕様がある

参考リンク

*1:正確には()の省略の有無は関係なく初期化子リストコンストラクタを使ったオブジェクト構築をリスト初期化と呼ぶような気がしますが、ここではわかりやすさのため、「()を省略して{}を使った初期化」を「リスト初期化」と説明しています。