2010-09-10

aggregateと初期化リストの不思議

ちょうど今、initializerの項目を執筆している。この部分は、結構難しい。分かりやすく説明しようとすれば、不正確になってしまうし、規格に忠実であることを求めると、規格のように無味乾燥とした、正しいが分かりにくい文章になってしまう。

このため、なかなか執筆が進まないのだが、このままではいけないので、ともかくこのブログで、何か解説をしてみようと思う。

たまたま2chのスレで、aggregateの話題が出ているので、これについて、なかなか複雑な部分を、解説する。

C言語では、配列や構造体(C言語の用語)を初期化リストで初期化できた。

struct Foo { int x ; int y ; } ;
struct Foo foo = { 1, 2 } ;

同じことは、C++でもできる。ただし、C++には「構造体」というものはない。すべて、クラスである。C++では、ある特殊な制限を満たした配列とクラスのことを、aggregate(アーグリゲート)という。aggregateは、初期化リストで初期化できる。

aggregateであるためには、配列かクラスで、
ユーザーによって提供されたコンストラクターがないこと、
非staticデータメンバーに初期化子がないこと、
privateやprotectedな非staticデータメンバーがないこと(言い換えれば、publicのみ)、
基本クラスがないこと、
仮想関数がないこと、
という条件を満たす必要がある。

このうち、「非staticデータメンバーに初期化子がないこと」という条件だけは、C++0xからの新機能であるので、解説が必要である。C++では、非staticなデータメンバーにも、初期化子を書く事ができる。

struct Foo
{
    int x = 0 ;
    Foo() { } 
    Foo( int x ) : x(x) { }
} ;

Foo a ; // a.x = 0
Foo b(1) ; // b.x = 1

このように、非staticなデータメンバーに初期化子を書くと、コンストラクターのメンバー初期化が省略された場合、その初期化子で初期化される。

ということは、当然、ユーザーによって提供されたコンストラクターがあるクラスは、aggregateではないので、C言語のような初期化はできない。

struct S
{
    S() { }
    int x ;
} ;

S s = { 0 } ; // エラー

ところで、このaggregateの定義には、ひとつ疑問がある。非staticなデータメンバーがどのようなクラスであるかについては、何も言及していないのである。もし、データメンバーも、同じ条件を見做さなければならないとすれば、aggregateではないデータメンバーを持たない、などといった文面があるはずである。しかし、そのような文面はない。したがって、非staticなデータメンバーは、コンストラクターも持てれば、アクセス指定子も使えるし、仮想関数も使えるはずである。

struct A
{
    int x ;
    struct B
    {
        B(int, int) { }
    } b;
} ;

int main()
{
    A a = { 0, {0, 0} } ;
}

実は、この文面は、非staticデータメンバーの初期化子を除けば、C++98から全く変わっていない。したがって、クラスAは、C++98でもaggregateである。したがって、このコードは、規格上疑いようもなくwell-formedである。

gccは、このコードを通す。MSVCは、「'A::b' : non-aggregates cannot be initialized with initializer list」などというエラーを吐く。これは、規格上間違っている。

またひとつ、MSVCを使うべきではない理由を発見したわけだ。この分では、C++0xの初期化リストの対応も、あまり期待できそうにはない。

次に、よく、構造体や配列を、ゼロで初期化するのに、以下のような記述が使われる。

int a[100] = {0} ;

これは、C言語では正しい。しかし。C++では、何もこのように書く必要はない。

C言語では、空の初期化リスト、{}は書けない。C言語は、その文法上、必ず何かひとつは、初期化リストに式が入っていなければならない。そして、初期化リストによって初期化する際に、配列や構造体のメンバーに、対応する初期化リストの値がない場合、staticストレージと同じように初期化される。staticストレージは、必ずゼロで初期化されることが保証されている。したがって、上記のコードは、「a[0]を0で初期化し、残りの要素をstaticストレージと同じ方法で初期化せよ」という意味である。

C++では、空の初期化リストを書く事ができるようになった。そこで、上記のコードは、以下のように書ける。

int a[100] = {} ;

これは、「配列のすべての要素をstaticストレージと同じ方法で初期化せよ」という意味である。

もちろん、まともなコンパイラーならば、実際に生成されるコードに違いはないだろう。しかし、C++の方が分かりやすい。これはいい改良である。

しかし、ほぼすべての参考書で、配列の要素や構造体のメンバーをすべてゼロで初期化するには、{0}で初期化すればよいという記述がなされている。そこで、現実のC++のコードも、多くは{0}を使っている。思うに、一部の参考書の筆者も、{0}とはどういう意味なのかということを、まともに考えたことがないのではあるまいか。ただ、{0}を、特別な文法か何かのように考えているのではあるまいか。

ちなみに、C++0xでは、リスト初期化がプログラマにも提供されたため、aggregate以外でも、リスト初期化できる。これには通常、std::initializer_listを使う。しかし、すばらしいことに、通常のコンストラクターも考慮される。つまり、以下のように書ける。

struct S
{
    S(int, int, int) { }
} ;

S s = { 1, 2, 3 } ;

追記:よく読んだら、C++98およびC++03には、コンストラクターを考慮するという機能がないので、

struct A
{
    int x ;
    struct B
    {
        B(int, int) { }
    } b;
} ;

このクラスは、aggregateだが、初期化リストでは初期化できないということになる。

1 comment:

Anonymous said...

> このクラスは、aggregateだが、初期化リストでは初期化できないということになる。

aggregate であれば初期化リストで初期化できますよ。
struct A a = { 1, B(2, 3) };
A::B は aggregate ではないので、 A::B は初期化リストで初期化できない、
ということですね。