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だが、初期化リストでは初期化できないということになる。

2 comments:

Anonymous said...

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

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

Anonymous said...

つまりMSVCの吐いた「'A::b' : non-aggregates cannot be initialized with initializer list」は正しいエラーメッセージだったわけですか。
MSが嫌いなのは別にいいけど、それで目を曇らせてたら完全に駄目な方のアンチですね。
そして誤解でディスっといて触れもしない・・・・流石。

>ちなみに、C++0xでは~
>よく読んだら、C++98およびC++03には
よく読む前にC++0xから出来るようになったことがそれ以前でどんな挙動を意味するのかは疑問に思うと思うの・・・