テンプレート関数の明示的インスタンス生成で解説したように,テンプレートクラスとかテンプレート関数を明示的にインスタンス化しておくことで, インスタンス化しておくことで,
- ヘッダファイルの肥大化(コンパイル速度の低下)
- 望ましくない実装の公開
といった問題を避けられる.一方,テンプレートクラスやテンプレート関数で,ある型についてのみ特殊な実装を行う,「テンプレートの特殊化」という技術がある(過去の記事で何度か使っている).本記事では,インスタンス化と特殊化を同時に使うと,思わぬ落し穴にはまることを,サンプルを交えながら解説する.
Part 1 †
以下の内容は標準規格などで確認できていないので,g++(4.3.3)に限った話かもしれないことを注意しておく.
まず,以下の3つのファイルからなるユニット unit1 を用意する:
ヘッダ:
/*! \file unit1.h
\date Feb.19, 2009
*/
#ifndef unit1_h
#define unit1_h
#include <iostream>
namespace hogehoge
{
template <typename T>
struct TTest
{
T x;
void print (void) const;
};
} // end of namespace hogehoge
#endif // unit1_h
ここでは,テンプレート構造体 TTest を定義している. TTest<T>::print の実装はこのファイルでは与えられていないから,これを include するだけではこの構造体を利用できない.
実装ヘッダ:
/*! \file unit1_impl.h
\date Feb.19, 2009
*/
#ifndef unit1_impl_h
#define unit1_impl_h
#include "unit1.h"
namespace hogehoge
{
template <typename T>
void TTest<T>::print (void) const
{
std::cout<<"x is "<<x<<std::endl;
}
} // end of namespace hogehoge
#endif // unit1_impl_h
ここでは,TTest<T>::print の実装を与えている.よって, unit1_impl.h を include すれば,すべての型に対して TTest が利用できるようになる.
特殊化と明示的インスタンス化:
/*! \file unit1.cpp
\date Feb.19, 2009
*/
#include "unit1.h"
#include "unit1_impl.h"
#include <iomanip>
namespace hogehoge
{
using namespace std;
// specialization (特殊化)
template <>
void TTest<int>::print (void) const
{
cout<<"x= 0x"<<hex<<x<<dec<<endl;
}
// explicit instantiation (明示的インスタンス化)
template class TTest<int>;
template class TTest<double>;
} // end of namespace hogehoge
このファイルでは,コンパイルコストを下げる目的で, TTest<T> を T=int と T=double に対して明示的にインスタンス化している.さらに, TTest<int>::print については特殊な実装を与えている(部分的特殊化).この unit1.cpp をコンパイルして生成されたオブジェクトファイルをリンクすれば, unit1.h の include だけで, TTest<int> と TTest<double> が使えるようになる.つまり, unit1_impl.h (実装) を include する必要がなくなるのだ.
確認プログラム:
/*! \file main1.cpp
\date Feb.19, 2009
*/
#include "unit1.h"
using namespace hogehoge;
int main(int argc, char**argv)
{
TTest<int> a={10};
TTest<double> b={2.5};
a.print();
b.print();
return 0;
}
このプログラムで unit1 を確認しよう.コンパイルは以下の順で行う.
g++ -Wall unit1.cpp -c g++ -Wall main1.cpp -c g++ -Wall main1.o unit1.o
実行すると
x= 0xa x is 2.5
という結果が得られ,特殊化に成功していること,インスタンス化によって main1.cpp から unit1_impl.h を include する必要がなくなっていることがわかる.理解できない人は, unit1.cpp のインスタンス化の部分をコメントアウトしてみよう.リンカエラーが起きるはずだ.
さて.ここからが本題だ.別のユニット unit2 を用意する.
ヘッダ:
/*! \file unit2.h
\date Feb.19, 2009
*/
#ifndef unit2_h
#define unit2_h
#include <string>
namespace hogehoge
{
void str_print (const std::string &str);
} // end of namespace hogehoge
#endif // unit2_h
ここでは, str_print という関数を定義しているだけ.
実装:
/*! \file unit2.cpp
\date Feb.19, 2009
*/
#include "unit2.h"
#include "unit1.h"
#include "unit1_impl.h"
// TTest<T> は T=string に対してインスタンス化されていないから
// 実装ファイルを include する必要がある
namespace hogehoge
{
using namespace std;
void str_print (const std::string &str)
{
TTest<string> test= {str};
test.print();
}
// void dummy (void)
// {
// TTest<int> test1= {50};
// TTest<double> test2= {5.0};
// test1.print();
// test2.print();
// }
} // end of namespace hogehoge
ここでは, str_print の実装を与えている.この関数では TTest<string> を利用していて,これは unit1 でインスタンス化されていないから, unit1_impl.h を include していることに注意しよう. dummy は今は気にしない.
確認プログラム:
/*! \file main2.cpp
\date Feb.19, 2009
*/
#include "unit1.h"
#include "unit2.h"
using namespace hogehoge;
int main(int argc, char**argv)
{
str_print ("hoge");
TTest<int> a={10};
TTest<double> b={2.5};
a.print();
b.print();
return 0;
}
コンパイルは,
g++ -Wall unit1.cpp -c ar r unit1.a unit1.o g++ -Wall unit2.cpp -c g++ -Wall main2.cpp -c g++ -Wall unit2.o main2.o unit1.a
とする.2行目でアーカイブ化しているのは, unit1 がライブラリだと想定しているからだ(これも後の話と絡む).実行すると,
x is hoge x= 0xa x is 2.5
という結果が得られ,ちゃんと TTest<int>::print が特殊化されているし,何も問題がない.
ところが,unit2.cpp の dummy 関数のコメントアウトを消して,上と同じ手順でコンパイル,実行してみよう.すると,
x is hoge x is 10 x is 2.5
という結果になる.なんと,TTest<int>::print の特殊化が消えているではないか.
なぜこんなことが起こったのか.
unit2.cpp の dummy 関数で TTest<int>::print を使用したことにより, unit2 で TTest<int> が生成されたから.
と考えるかも知れない.が,そうではない.ためしに dummy 関数を
void dummy (void)
{
TTest<int> test1= {50};
// TTest<double> test2= {5.0};
test1.print();
// test2.print();
}
のように変えてみよう.まだ TTest<int>::print を使用しているため, unit2 で TTest<int> が生成される.だが,この結果は,
x is hoge x= 0xa x is 2.5
となる.特殊化が有効になっている.
おそらく,正解は以下のようなものだ(と考えられる):
- unit2 で TTest<int>::print と TTest<double>::print を使用したことにより, unit1.a に含まれる unit1.o をリンクする必要がないとリンカが判断した
- このため unit1.o をリンクせずに実行ファイルが生成された
もし, unit1.a ではなく, unit1.o を直接リンクしていたら,特殊化が有効になっているという事実が,これを裏付けている:
g++ -Wall unit1.cpp -c g++ -Wall unit2.cpp -c g++ -Wall main2.cpp -c g++ -Wall unit2.o main2.o unit1.o
のようにコンパイルし実行すると(dummyはまったくコメントアウトされていないものを使用),
x is hoge x= 0xa x is 2.5
のように特殊化された結果が得られるのだ.
ここまでのまとめ. †
上のような結果は,テンプレートの特殊化と明示的インスタンス化を同時に使うことで発生し,コンパイル時には発見されない,原因を見付けにくいバグとなり得る.頻繁に起きる問題ではないが,だからこそ解決しにくいのではないかと思われる.テンプレートの特殊化と明示的インスタンス化を同時に使う場合はご注意.
で,対策は? †
Part 2 に続く.
Part 2 †
Part 1で扱った問題の対処法について.結論:テンプレートクラスの(部分)特殊化を行う場合は,そのテンプレートクラスを宣言しているヘッダファイルで「(部分)特殊化の宣言」を行うこと.部分特殊化の実装は,通常のクラスまたはメンバ関数の実装と同様にすること(inline 関数やテンプレート関数ならその場で実装,そうでないならユニットの実装ファイルに実装を記述し,実装が複数のオブジェクトファイルで重複しないようにする).
Part 1の例の場合, unit1.h を以下のように変更する:
/*! \file unit1.h
\date Feb.19, 2009
*/
#ifndef unit1_h
#define unit1_h
#include <iostream>
namespace hogehoge
{
template <typename T>
struct TTest
{
T x;
void print (void) const;
};
// 追加:
// declaration of specialization (特殊化の宣言)
template <>
void TTest<int>::print (void) const;
} // end of namespace hogehoge
#endif // unit1_h
ここで追加された部分特殊化の宣言により,このヘッダを include するソースでは TTest<T>::print が T=int に対して部分特殊化されていることを知ることができる.この特殊化された関数は unit1.o でしか定義されていないから,リンカは確実に unit1.o をリンクするようになり,先日の記事のような問題は生じない.
なお, unit1.h に TTest<int>::print の実装を書くと,複数のオブジェクトファイルで TTest<int>::print が定義されることになり,リンカが重複定義エラーを吐くので注意されたい.もちろんこの関数が inline 関数であれば,ここに定義を書けばよい.
コンパイル:
g++ -Wall unit1.cpp -c ar r unit1.a unit1.o g++ -Wall unit2.cpp -c g++ -Wall main2.cpp -c g++ -Wall unit2.o main2.o unit1.a
して実行すると:
x is hoge x= 0xa x is 2.5
のように,期待通りの結果が得られる.
何が問題か? †
今回のケースでの問題は,部分特殊化の宣言をちゃんと書かなかったことが原因だ.しかも,コンパイラやリンカがそれを指摘してくれないことが,問題を深めている.実行ファイルはちゃんと生成され,しかも場合によっては期待通りに動き,ある特定の場合にしか期待を裏切る振舞をしないことが問題なのである.
テンプレートクラスの「特殊化」と「明示的インスタンス生成」を同時に使う場合は注意だ.というよりも,このような場合に限らず,普段から「特殊化」する場合には「特殊化の宣言」をテンプレートと同じヘッダ内に書くようにするべきだろう.