はじめに
C++で書かれたソースコードをコンパイルすると、ソースコードに書かれている関数がバイナリに埋め込まれ、呼び出し可能な状態で保存される。
しかし、GCCにおいてはある条件下で全く同名の関数が埋め込まれるということが分かった。
今後同じような状況に遭遇した人が混乱しないように記事を残しておく。
実験
問題のないケース
int foo();
#include "foo.hpp"
int foo() { return 1; }
これは全く問題ない。foo関数がバイナリに埋め込まれるのは一度だけである。
$ g++ -c foo.cpp
$ nm --demangle foo.o
0000000000000000 T foo()
シンボルがT
となっているのは、バイナリに実装が埋め込まれていることを示している。
問題のケース
ことが起きたのは、コンストラクタやデストラクタを定義した場合である。
struct Bar {
int var;
Bar();
~Bar();
};
#include "bar.hpp"
Bar::Bar() { var = 1; }
Bar::~Bar() { var = 2; }
先ほどと同様にコンパイルしてみると、奇妙な現象が起こる。
$ g++ -c bar.cpp
$ nm --demangle bar.o
0000000000000000 T Bar::Bar()
0000000000000000 T Bar::Bar()
0000000000000016 T Bar::~Bar()
0000000000000016 T Bar::~Bar()
なんと、全く同名の関数が2つずつ埋め込まれている。しかも、T
というシンボルは実装が埋め込まれていることを示している。
私が最初これを見た時、多重定義(ODR違反)ではないか?またGCCのバグか?と思ったがそんなことはないようだ。
Two identical constructors emitted? That's not a bug!
これによると、GCCでは一つのコンストラクタから以下のような関数群を生成する。
- complete object constructor
- オブジェクトそのものを生成するための関数
- base object constructor
- 基底クラスのコンストラクタとして呼び出した際に使用される関数。ただし、仮想継承を用いていない場合はcomplete object constructorと同じ実装を用いる。
- complete object allocating constructor
- オブジェクトの生成後、
operator new(sizeof(class))
を呼び出して、メモリを確保するための関数。通常この関数は生成されず、他のコンパイラを併用する場合などに用いられる(参考: Why are C3 allocating constructors never used?)。
デストラクタに対しても同様の関数群が生成される。
またこれらの関数はマングリングした後の名前が異なるが、その名前をデマングルするといずれも同じ名前になるため、同一の関数が複数存在するように見える。
先程のbar.o
の中身を、デマングルせずに表示してみよう。
$ nm bar.o
0000000000000000 T _ZN3BarC1Ev
0000000000000000 T _ZN3BarC2Ev
0000000000000016 T _ZN3BarD1Ev
0000000000000016 T _ZN3BarD2Ev
なるほど、確かに、マングリング後の名前は異なっている。
クラス名の後にC1
と書いてあるものがcomplete object constructorで、C2
はbase object constructorである。
関数のアドレス(offset)が一致しており、同じ実装を用いていることが分かる。
したがって、これは全くもって正常な動作であり、GCCのバグなどではなかったのだ!
このことはGCCのサイトのNon-bugsというセクションにも記載されている。
まとめ
GCCにおいて、コンストラクタやデストラクタの実装を含むソースコードをコンパイルすると全く同名の関数がバイナリに埋め込まれるが、これはGCC本来の仕様で、正常に動作するために必要なものである。