g++ がコンストラクタ二つ作る件

高林さんのとこでチラっと出ていたコンストラクタの実体が二つある件について。

http://namazu.org/~satoru/blog/archives/000061.html

コンストラクタ Tax::Tax() が 2つあるのは、片方はオブジェクトの作成用、もう片方は継承されたとき用のようです。

ここに理由が書いてあります!(たぶん)

http://www.codesourcery.com/cxx-abi/abi.html#vtable-ctor

読んでもイマイチわからんというか面倒そうなので正直読んでません、が、なんとなく理由はわかった気がしました。

例えばこんなコード。

struct A {
    A();
};
struct B : public A {
    B();
};
struct C : public B {
    C();
};

逆アセしてやると、 C::C() では B::B() を呼び、 B::B() は A::A() を呼んでました。二つのコンストラクタともにやってることは一緒でした。これはなんの問題もないため、コンストラクタ二つもイラネーと苦情が出るのでした。

でもこのケース。

struct A {
    A();
};
struct B : public virtual A {
    B();
};
struct C : public virtual A, public virtual B {
    C();
};

このケース、多重継承なので C::C() が呼ぶべきコンストラクタは A::A() と B::B() の二つ。これで B::B() まで A::A() を呼び出していた場合、 C::C() から A::A() が合計二度呼び出されてしまうわけでして。そんなわけでこの場合は B::B() と C::C() の二種類目のコンストラクタは内容が変わっていて、それぞれ親クラスのコンストラクタを呼ばないようになっています。子クラスから呼ぶ場合はそっちのコンストラクタを呼ぶことによって、 A::A() が二度呼ばれることを回避してるみたいです。

まとめると

といった感じ。

そこで気になるのがこの ABI 変更がある前。山程 GCC が入ってる意味が久々にありますよ、っと GCC-2.95.4 でコンパイルして逆アセしてみると、なんか引数無いコンストラクタなのに引数取ってないかこれ、とシンボル見てみると、

080484e8 T A::A(void)
08048508 T B::B(int)
0804854c T C::C(int)

ダサい。勝手に引数が加わってやがります。呼ばれてるコンテキストによってフラグを渡して、それで動作を分岐している模様。 ICC でもやってみたところ、合計3種類のコンストラクタが生成されていて、うち1つがフラグを必要とする外部に隠蔽されているコンストラクタで、残り2つがその隠しコンストラクタを違う引数で呼び出すコンストラクタでした。つまりコンストラクタが大きい時に同じコンストラクタを二つ作るとサイズが大きくなってしまうからこうしてるのだと思います。

ていうか最近の g++ はデカいコンストラクタでも遠慮なく二つ作りやがるのでコンストラクタは小さくしようということかこれは。

あと気になったことは、二つ実装があるってことはデバッガどうなるん?と。

(gdb) break abi.cc:13
Breakpoint 1 at 0x8048468: file abi.cc, line 13.
Breakpoint 2 at 0x804848a: file abi.cc, line 13.
warning: Multiple breakpoints were set.
warning: Use the "delete" command to delete unwanted breakpoints.

ちゃんと両方止めてくれました。

ていうか問題は、「俺は virtual 継承使わんからコンストラクタは一個にしやがれ!」ってオプションが無いように見えることか…

さらにどうでもいい話題に…

#ifdef VIRTUAL
struct A {
    A(int i);
};
struct B : public virtual A {
    B(int i);
};
struct C : public virtual A, public virtual B {
    C(int i);
};
#else
struct A {
    A(int i);
};
struct B : public A {
    B(int i);
};
struct C : public B {
    C(int i);
};
#endif

A::A(int i) {
    printf("A%d\n", i);
}
B::B(int i) : A(i+1) {
    printf("B%d\n", i);
}
C::C(int i) :
#ifdef VIRTUAL
    A(i+1), 
#endif
    B(i+1)
{
    printf("C%d\n", i);
}

int main() {
    A a(0);
    B b(0);
    C c(0);

    return 0;
}

あたりまえだけど、 -DVIRTUAL つけたりつけなかったりで動作変わるのね…

> g++ abi.cc
> ./a.out
A0
A1
B0
A2
B1
C0
> g++ abi.cc -DVIRTUAL
> ./a.out
A0
A1
B0
A1
B1
C0
なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h