問題となるケース
以下のようなコードをコンパイルする。
#include <vector>
template <typename T>
struct Foo {
#ifdef USE_DEFAULT
Foo() = default;
#else
Foo() {}
#endif
std::vector<int> x;
};
#include "foo.h"
template struct Foo<int>;
Foo() {}
のほうだと
$ g++ -std=c++11 -c foo.cc
$ nm -C foo.o | grep Foo
0000000000000000 W Foo<int>::Foo()
0000000000000000 W Foo<int>::Foo()
0000000000000000 n Foo<int>::Foo()
のようにコンストラクタのコードが生成されていることがわかる。
一方、 Foo() = default;
だと
$ g++ -std=c++11 -c -DUSE_DEFAULT foo.cc
$ nm foo.o
(出力なし)
のように生成されない。
これは g++ のバグのようだ → https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57728
C++11 以降だと、テンプレートの明示的インスタンス化は、利用側での extern template と併せて使われることが多いと思うが、その場合このコンストラクタの定義が生成されていないため、リンク時にエラーになる。
確認は以下の g++ 5.4.0 で行ったが、最新の 7.2.0 でも同様の結果だったので直っていないようだ。
$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
インライン ?
extern template でインスタンス化を抑制しているからといって、インライン化しないわけではない。
インライン化された場合にはこのバグは問題にならない。
上の例の foo.h
を利用する以下のコードを考える。
#include "foo.h"
extern template class Foo<int>;
int main() {
Foo<int> foo;
}
最適化オプションなしでコンパイルすると以下のようになる。
$ g++ -std=c++11 -c -DUSE_DEFAULT main.cc
$ nm -C main.o | grep Foo
U Foo<int>::Foo()
0000000000000000 W Foo<int>::~Foo()
0000000000000000 W Foo<int>::~Foo()
0000000000000000 n Foo<int>::~Foo()
コンストラクタのシンボルを参照している。しかしこのコンストラクタは生成されていないから、リンク時にエラーとなる。
$ g++ -std=c++11 -DUSE_DEFAULT main.cc foo.cc
/tmp/ccQHJquT.o: In function `main':
main.cc:(.text+0x1f): undefined reference to `Foo<int>::Foo()'
collect2: error: ld returned 1 exit status
一方、 -O2
をつけてみると
$ g++ -std=c++11 -c -DUSE_DEFAULT -O2 main.cc
$ nm -C main.o | grep Foo
(出力なし)
$ g++ -std=c++11 -DUSE_DEFAULT -O2 main.cc foo.cc
$ echo $?
0
と問題なくリンクできる。 インライン化により、コンストラクタ呼び出しが消滅しているためである。
対処法
- 明示的インスタンス化するクラスでは
= default
を使わない - extern template を使わない
のような消極的な方法しか思いつかない…。
まとめ
C++ テンプレートクラスの明示的インスタンス化の際、 =default
で定義された関数が生成されない g++ のバグについて書きました。