2010-03-21

initializer listの解説

C++の思想の一つに、組み込み型と、ユーザー定義型との、区別をなくすという理念がある。したがって、C++では、組み込み型だろうか、クラスだろうが、自動ストレージ、静的ストレージ、動的ストレージ上に、構築できるし、演算子をオーバーロードできる。C++の多くの機能が、組み込み型とクラスとの、区別をなくすよう、考案されてきた。

C++では、配列や構造体の初期化に、特殊な構文を使える。

int x[3] = { 1, 2, 3 } ;

struct Foo { int i ; double d ;  } ;

Foo foo = { 123, 3.14 } ;

これは、クラスでは、使えなかった。C++0xでは、これができるようになる。

とはいうものの、これは、特に解説を要するほどのものでもないのだ。

まず、この{}による初期化式を、リスト初期化という。ユーザー定義のコンストラクター、privateやprotectedな非静的なデータメンバー、基本クラス、仮想関数のないクラス、それと配列は、従来通りの初期化ができる。上と同じ例を示す。

int x[3] = { 1, 2, 3 } ;

struct Foo { int i ; double d ;  } ;

Foo foo = { 123, 3.14 } ;

それでは、「ユーザー定義のコンストラクター、privateやprotectedな非静的なデータメンバー、基本クラス、仮想関数」を有するクラスは、どうやって、リスト初期化すればいいのか。これは、クラス側で、リスト初期化に対応するコードを書く必要がある。

言語サポートライブラリの、std::initializer_list<T>というクラスを、引数に取る必要がある。これは、<initializer_list>をincludeすることによって、使うことができる。

#include <initializer_list>

class Foo
{
private :
    int sum ;

public :
    Foo( std::initializer_list<int> list )
        : sum(0)
    {
        for ( auto iter = list.begin() ; iter != list.end() ; ++iter )
        {
            sum += *iter ;
        }
    }

} ;

int main()
{

{// 初期化
    Foo foo0( { } ) ;
    Foo foo1( { 1 } ) ;
    Foo foo2( { 1, 2 } ) ;
    Foo foo3( { 1, 2, 3 } ) ;
}

{// これも同じ初期化
    Foo foo0 =  { } ;
    Foo foo1 = { 1 } ;
    Foo foo2 = { 1, 2 }  ;
    Foo foo3 = { 1, 2, 3 } ;
}

{// 実は、これでもいい
    Foo foo0{ } ;
    Foo foo1{ 1 } ;
    Foo foo2{ 1, 2 } ;
    Foo foo3{ 1, 2, 3 } ;
}

}

このように書くことができる。最後の例は、コンストラクタの呼び出しに、()の代わりに、{}を使っている例である。C++0xでは、()の代わりに、{}も使えるようになった。どちらも、同じ意味である。ただし、()を使った場合、以下のようなことはできない。

// これは、関数のプロトタイプ宣言。
// 引数を取らず、Foo型の戻り値を返す、wrongという名前の関数の宣言。
// Foo wrong(void) 
Foo wrong() ;

// これは、Foo型の、okeyという名前の変数の宣言。
Foo okey{} ;

()を使った場合は、文法上、関数の宣言と曖昧になってしまう。{}ならば、そのようなことはない。

もちろん、初期化と代入は違う。代入でもリスト初期化を使えるが、その場合は、別に代入演算子も定義しなくてはならない。

struct Foo
{
    Foo() {}
    Foo & operator = ( std::initializer_list<int> list ) {}
} ;

int main()
{
    Foo foo ;
    foo = { 1, 2, 3 } ;
}

これは、普通の関数でも使える。

void f( std::initializer_list<int> list ) {}

template < typename T >
void g( std::initializer_list<T> list ) {}

template < typename T >
void h( T x ) {}


int main()
{
    f({1,2,3}) ;

    // g<int>()が呼ばれる。
    g({1, 2, 3}) ;
    // g<double>()が呼ばれる。
    g({1.0, 2.0, 3.0}) ;

    // エラー。テンプレート引数を推定できない。
    h({1}) ;
}

また、添字でも使える。

class Array
{
private :
    int data[10][10][10] ;

public :
    int & operator [] ( std::initializer_list<int> list )
    {
        if ( list.size() != 3 )
            throw std::invalid_argument() ;

        auto iter = list.begin() ;
        int const x = *iter ; ++iter ;
        int const y = *iter ; ++iter ;
        int const z = *iter ;

        return data[x][y][z] ;
    }
} ;

int main()
{
    Foo foo ;
    foo[{ 1, 1, 1 }];
}

これだけのことである。{}を、初期化の際のコンストラクタへの引数の指定に使えるのだけは、少々違和感を感じるかもしれないが、{}の方が、文法上、曖昧にならないので、便利である。

リスト初期化は、同じ型しか渡せないという制限がある。もし、任意の数の、違う型の引数を渡したいのならば、Variadic templateを、代わりに使うことができる。

4 comments:

NyaRuRu said...

>言語サポートライブラリの、std::initializer_listというクラスを、引数に取る必要がある。

構文的には統一されたものの,実装はコンストラクタに頼る,という感じでしょうか.
第一印象ですが,const 修飾されたオブジェクトの初期化とはあまり相性が良くないよう見えます.
http://d.hatena.ne.jp/NyaRuRu/20090729/p1
http://cppemb.blog17.fc2.com/blog-entry-19.html

江添亮 said...

初期化リストを渡せるとして、それを単に、クラスの各データメンバーの初期化に使えるとは限らないわけです。
結局、クラスをどのように初期化するかは、クラスの作者次第です。
とすれば、言語機能的には、初期化リストで任意個の引数をコンストラクタに渡せれば、それで十分でしょう。

リンク先のコードは、Bjarne Stroustrupの言葉を借りると、

"such programs deserve to be broken"
Bjarne Stroustrup HOPL3

でしょうね。

江添亮 said...

const性に、暗黙的に穴を開けないというのは、もちろん、規格制定にあたっては、非常に考慮されています。
たとえば、これとか。
本の虫: C++相談室での素晴らしい疑問

NyaRuRu said...

>リンク先のコードは、Bjarne Stroustrupの言葉を借りると、

なるほど.

>const性に、暗黙的に穴を開けないというのは、もちろん、規格制定にあたっては、非常に考慮されています。

うーむ,せめて aliasing をコンパイル時に検出してくれるような仕組みなら色々応用が利くのに,といつも残念に思っています.
http://parashift.com/c++-faq-lite/const-correctness.html#faq-18.16