追記(2018/4/15)
extern template使ってる人いました。
https://github.com/panzi/formatstring/blob/master/include/formatstring/formatvalue.h
はじめに
C++11から、実はひっそりとextern templateという機能が追加された。
使ってる人は見たことないし、そもそもヘッダーに実装もまとめて書く人が多いから、目に触れる機会は殆ど無いと思う。
export templateが辿った運命と同じように、いずれは廃止されるかもしれない。
そんなextern templateを扱うこの記事も、南極のパックアイスに閉じ込められて身動きが取れなくなった一匹のナンキョクオキアミの命より虚しいものなのかもしれない。
なぜ今更こんな記事を書くのか??
extern templateもまたC++の歴史に名を刻むものであり、異質にして興味深い性質を持つ機能だからである。
templateの実体化再考
extern templateの機能に触れる前に、まずはtemplateの自体化について説明しておく。
C++のテンプレート関数及びテンプレートクラスは、テンプレート実引数が与えられるまで実体化されない。
つまり、テンプレート引数を与えられない限り、それらの関数やクラスは無視される。
例として、以下のような関数を考える。
#include <iostream>
template <typename T>
void println(T&& t) {
std::cout << t << std::endl;
}
ごく普通のテンプレート関数だ。このファイルをコンパイルしてみる。
nmコマンドは、ファイル内に書き込まれているシンボル(関数や型情報など)を表示するためのコマンドである。
$ g++ -std=c++11 -Wall -c example1.cpp
$ nm --demangle example1.o
U __cxa_atexit
U __dso_handle
000000000000003e t _GLOBAL__sub_I_example1.cpp
0000000000000000 t __static_initialization_and_destruction_0(int, int)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
おわかりだろうか
println関数がどこにも存在しない。これはコンパイラのバグか?
実はバグではない。コンパイラが意図的に無視した結果である。
その証拠に、他の関数からprintln関数に実引数を与えてみると、結果が異なる。
#include <iostream>
template <typename T>
void println(T&& t) {
std::cout << t << std::endl;
}
int main() {
// implicit instantiation
println(1);
return 0;
}
$ g++ -std=c++11 -Wall -c example2.cpp
$ nm --demangle example2.o
U __cxa_atexit
U __dso_handle
000000000000008a t _GLOBAL__sub_I_main
0000000000000000 T main
U __stack_chk_fail
000000000000004c t __static_initialization_and_destruction_0(int, int)
0000000000000000 W void println<int>(int&&)
U std::ostream::operator<<(int)
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
println関数の記述は無駄ではなかったのだ。
このように、他の関数から呼び出すことによってテンプレート関数やテンプレートクラスを実体化する方法を、implicit instantiation(暗黙的実体化)と呼ぶ。
暗黙的があるなら、明示的もあるのか?もちろん存在する。
explicit instantiation(明示的実体化)は、関数やクラスを呼び出すことなくテンプレートを実体化する方法である。
#include <iostream>
template <typename T>
void println(T&& t) {
std::cout << t << std::endl;
}
// explicit instantiation
template void println<int>(int&&);
explicit instantiationの使い道
ところでこのexplicit instantiationは、どのような場面で用いるのだろうか?
先に言っておくとヘッダーオンリーのライブラリではまず使う場面はない。
これは、テンプレート関数やテンプレートクラスの実装を共有ライブラリなどに埋め込むための機能だからである。
例として、ライブラリの開発者が以下のようなライブラリを提供しているとしよう。
#include <cstddef>
template <typename Tp>
class Vector {
public:
Vector();
explicit Vector(std::size_t size);
~Vector();
const std::size_t len();
Vector& append(Tp value);
private:
Tp* data;
std::size_t length;
};
#include <Vector.hpp>
// Vectorクラスの実装
template <typename Tp>
Vector<Tp>::Vector() {
data = nullptr;
length = 0;
}
...(省略)
// explicit instantiation
template class Vector<int8_t>;
template class Vector<int16_t>;
template class Vector<int32_t>;
template class Vector<int64_t>;
template class Vector<uint8_t>;
template class Vector<uint16_t>;
template class Vector<uint32_t>;
template class Vector<uint64_t>;
template class Vector<float>;
template class Vector<double>;
template class Vector<long double>;
このライブラリの使用者は、予めVector.cppをコンパイルして共有ライブラリとして使用する(実際には、ライブラリ開発者がCMakeLists.txtなどを提供している)。
g++ -std=c++11 -Wall -fPIC -c Vector.cpp
g++ -Wall -fPIC -shared -o libVector.so Vector.o
するとこの共有ライブラリには、様々な型に対するVectorクラスの実装が埋め込まれる。
つまりライブラリの使用者は、Vector.cppをコンパイルし直す必要がなく、Vector.hppをインクルードして共有ライブラリをリンクするだけでこのライブラリを使用できる。
例:
#include <iostream>
#include <Vector.hpp>
int main() {
Vector v(3);
std::cout << v.len() << std::endl; // => 3
return 0;
}
$ g++ -o main main.cpp -Wall -O2 -L. -lVector
$ ./main
3
「テンプレート関数やテンプレートクラスの実装はヘッダーに書かなければならない」とかいうデマが広がっているらしいが、予め与えられる型の候補がわかっていれば、このようにして実装を分けることが可能なのである。
extern templateの機能と役割
explicit instantiationが抱える問題点
しかしながら、explicit instantiationは大きな問題点を抱えている。
- 実装が存在しない型を実引数として与えるとコンパイルエラーとなる
- 共有ライブラリ(または静的ライブラリ)のファイルサイズが肥大化しやすい
例えば、上記のlibVector.soを読み込んで、以下のようにVectorクラスを実体化しようとする。
#include <complex>
#include <Vector.hpp>
int main() {
Vector<std::complex<float>> v;
return 0;
}
これは残念ながらコンパイルが通らない。std::complexに対する実装が存在しないため、リンクエラーとなるためである。
これに対抗する手段として、ライブラリ開発者は以下の2つの方法の選択を迫られる。
- ヘッダーオンリーのオプションを用意する
- extern templateを使用する
ヘッダーオンリーのオプションの追加は簡単。やり方は前回の記事に示してあるのでそちらを参照してほしい。
今回の記事では、extern templateを使った方法を紹介する。
extern templateの仕様
extern templateとは、implicit instantiationを強制的に抑止するための機能である。
まずはこちらのコードを見てほしい。
#include <iostream>
template <typename T>
void println(T&& t) {
std::cout << t << std::endl;
}
// extern template
extern template void println<int>(int&&);
int main() {
// implicit instantiation
println(1);
return 0;
}
先ほど紹介したexample2.cppのコードに、extern templateから始まるステートメントを1行追加しただけである。
このファイルをコンパイルしてみよう。
$ g++ -std=c++11 -Wall example4.cpp
/tmp/ccqsVbII.o: In function `main':
example4.cpp:(.text+0x2d): undefined reference to `void println<int>(int&&)'
collect2: error: ld returned 1 exit status
なんとリンクエラーを吐いてしまった。println関数の実装はすぐそこにあるのに!!!
何が起こったのか。実は、extern templateという宣言によって、println関数の一切のimplicit instantiationが禁止され、結果的にprintln関数が実体化されなかったということである。
これによって、実装を直接参照してコンパイルするのではなく、予めコンパイルされた共有ライブラリから実装を探してくることが可能になる。
extern templateの便利な使い道①
一見何でもなさそうな機能だが、extern templateはまさにexplicit instantiationの欠点を綺麗に補うものなのである。
先ほど紹介したVectorクラスをもとに考えてみよう。explicit instantiationの問題点は、
- 実装が存在しない型を実引数として与えるとコンパイルエラーとなる
- 共有ライブラリ(または静的ライブラリ)のファイルサイズが肥大化しやすい
というものであった。一つ目の問題点を解決するために、ヘッダーファイルに以下のステートメントを追加する。
// extern template
extern template class Vector<int8_t>;
extern template class Vector<int8_t>;
extern template class Vector<int16_t>;
extern template class Vector<int32_t>;
extern template class Vector<int64_t>;
extern template class Vector<uint8_t>;
extern template class Vector<uint16_t>;
extern template class Vector<uint32_t>;
extern template class Vector<uint64_t>;
extern template class Vector<float>;
extern template class Vector<double>;
extern template class Vector<long double>;
// integrate definition
#include "Vector.cpp"
ここで書き連ねているextern templateは、ライブラリ使用者がいちいちコンパイルするたびにVectorクラスが実体化されるのを防ぐためのものである。
続いて、Vector.cppのほうで書かれていたexplicit instantiationであるが、ライブラリのビルド時にだけ有効となるようにマクロを制御する。
#ifdef VECTOR_BUILDING
// explicit instantiation
template class Vector<int8_t>;
template class Vector<int16_t>;
template class Vector<int32_t>;
template class Vector<int64_t>;
template class Vector<uint8_t>;
template class Vector<uint16_t>;
template class Vector<uint32_t>;
template class Vector<uint64_t>;
template class Vector<float>;
template class Vector<double>;
template class Vector<long double>;
#endif
これで完了である。Makefileなどは変える必要はない。
これでもう、Vectorクラスは任意の型に対応できるクラスとなった。ライブラリに埋め込まれているものはそちらの実装を、埋め込まれていないものはその場でコンパイルといったように自動的に制御される。
次に2つめの問題点に触れてみる。もはやVectorクラスは任意の型に対応できるので、explicit instantiationをいくつか減らしても問題ない。そこで、よく使われる型のみに絞ってexplicit instantiationを行うようにする。
//explicit instantiation
template class Vector<int32_t>;
template class Vector<int64_t>;
template class Vector<uint8_t>;
template class Vector<double>;
もちろんextern templateのほうも同じように書き換える。これで共有ライブラリのファイルサイズをかなり節約できるはずだ。
extern templateの便利な使い道②
extern templateを使えば、もはやテンプレート関数やテンプレートクラスの宣言と実装を分離する必要もない。
先ほどのVectorクラスで言えば、もはや実装もまとめてヘッダーに書いてしまえばよいのだ。
#include <cstddef>
#include <cstdint>
template <typename Tp>
class Vector {
public:
Vector() {
data = nullptr;
length = 0;
}
...(省略)
private:
Tp* data;
std::size_t length;
};
extern template class Vector<int32_t>;
extern template class Vector<int64_t>;
extern template class Vector<uint8_t>;
extern template class Vector<double>;
#include <Vector.hpp>
template class Vector<int32_t>;
template class Vector<int64_t>;
template class Vector<uint8_t>;
template class Vector<double>;
これは、多くの人が好む「ヘッダーオンリー」の形にかなり近いと思う。
「テンプレート関数やテンプレートクラスの実装はヘッダーに書かなければならない」というデマは、強ち間違いではなかったのだ。
まとめ
以上のように、extern templateとexplicit instantiationの組み合わせによって、リンクの自動制御が可能となる。
もはやexplicit instantiationによる型制約はなくなり、任意の型を受け付けられるようになった。
ヘッダーオンリーのライブラリを好む人も、一度共有ライブラリの世界を味わってほしい。
コンパイル時間の削減に興味がある方は、ぜひとも前回の記事を読んでみると良いだろう。