テンプレートを汎用的な構造体に対して特殊化するには
template<typename T> function(const T&)のようなテンプレート関数があったとして,この実装を T が構造体の場合は変更したい(部分特殊化したい)とする.特定の構造体に対する特殊化なら話は簡単だが,本記事では汎用的な構造体に対する部分特殊化を扱う.この問題は,少々厄介だ.
前提: そもそも,関数は部分特殊化できない
関数に対する部分特殊化は,C++では禁止されている.そこで,テンプレート関数オブジェクトを作って,これを部分特殊化する.まず,特殊化,テンプレート関数オブジェクト,部分特殊化について説明する.理解している人は,前提を飛ばしても問題ない.
特殊化
テンプレート関数
template <typename T> void Print(const T &x) { ... // 一般的な T に対する実装 }
を int 型について特殊化する(特別な実装を与える)には,
template <> void Print<int>(const int &x) { ... // int に対する実装 }
のように書く.すると,Print(3) などに対しては,int 型固有の実装が実行される.なお,Print<int>の<int>は省略できる.
テンプレート関数オブジェクト
Print テンプレート関数を,テンプレート関数オブジェクトに変更すると,
template <typename T> struct TPrintObj { void operator()(const T &x) { ... // 一般的な T に対する実装 } };
となる.関数(operator())がオブジェクト(TPrintObj)の中に内包されている.このメリットは,関数オブジェクト - Wikipediaなどを参照.
関数オブジェクトを int 型について特殊化するには,
template <> struct TPrintObj<int> { void operator()(const int &x) { ... // int に対する実装 } };
のように書く.この場合,TPrintObj の後ろの<int>は省略できない.
部分特殊化
テンプレート関数オブジェクトは,部分特殊化ができる.例えば TPrintObj を list<T> に対して特殊化する(T はテンプレート仮引数)ような場合を,部分特殊化と呼ぶ.つまり,特殊化しようとする型が,まだテンプレートの性質を持っている場合の特殊化だ.
具体的には,
template <typename T> struct TPrintObj<list<T> > { void operator()(const list<T> &x) { ... // list<T> に対する特殊化 } };
のように書けばよい.
少し脱線.
前述の Print テンプレート関数を, list<T> に対して部分特殊化できるか実験してみる.つまり,
template <typename T> void Print(const list<T> &x) { ... // list<T> に対する特殊化 }
のように書くのである.予想に反して,このコンパイルは通る.しかし,これはテンプレート関数が部分特殊化されたからではなく,関数のオーバーロードが行われたからだ.その証拠に,
template <typename T> void Print<list<T> >(const list<T> &x)
のように省略せずに部分特殊化の宣言を書くと,コンパイルエラーとなる*1.オーバーロードでいいじゃないか,という気がするが,Print があるクラスに内包されたメンバ関数である場合には無理だ.つまり,クラス外部からオーバーロードを追加することなどできないため,部分特殊化をせざるをえず,(メンバ)関数の部分特殊化は許されていないのでエラーとなるのだ.要するに,テンプレート関数オブジェクトを使う必要がある.
脱線終わり.
汎用的な構造体に対して特殊化したい
関数オブジェクト template<typename T> struct TPrintObj を汎用的な構造体 T について部分特殊化したいとする.なお,断っておくが,これはかなり特殊なケースだ.なぜなら,対象となるすべての構造体 T は同じインターフェイスを持つ必要があり,それならば,共通の基底構造体を作って TPrintObj を特殊化(部分ではない)しておけば解決できるからだ.
いずれにせよ,TPrintObj を汎用的な構造体 T について部分特殊化する.早い話,
template <typename T> struct TPrintObj<struct T> { void operator()(const struct T &x) { ... // struct T に対する特殊化 } };
のような感じでコードが書ければよいのだが,これはコンパイルエラーとなる*2.
そこで,解決策として,
template <typename t_type> struct TStruct { typedef t_type T; T &Entity; TStruct(T &entity) : Entity(entity) {} };
のような,構造体(実はどんな型の変数でもいいが)をラップするテンプレート構造体を作り,これを用いて汎用的な構造体の部分特殊化を実現する.つまり,汎用的な構造体に対する特殊化を
template <typename T> struct TPrintObj<TStruct<T> > { void operator()(const TStruct<T> &x) { ... // TStruct<T> に対する特殊化 } };
と実装するのだ.operator() で,x.Entity は T 型(構造体を想定)だから,構造体 T に対する特殊化にほぼ等しい.
なお,実際に使うときのために,TStruct を任意の構造体に対して生成するテンプレート関数
template <typename T> TStruct<T> Struct(T &entity) {return TStruct<T>(entity);}
を定義しておくと便利だ(テンプレート構造体のテンプレート実引数は省略できないから.要するにコードを省略できる).
サンプルプログラム
#include <iostream> #include <list> using namespace std; // テンプレート関数オブジェクト template <typename T> struct TPrintObj { void operator()(const T &x) { cout<<"value is "<<x<<endl; } }; // 補助関数 template <typename T> void Print(const T &x) { TPrintObj<T>()(x); } // int に対する特殊化 template <> struct TPrintObj<int> { void operator()(const int &x) { cout<<"value(int) is "<<x<<endl; } }; // list<T> に対する部分特殊化 template <typename T> struct TPrintObj<list<T> > { void operator()(const list<T> &x) { cout<<"values(list) are "; for(typename list<T>::const_iterator itr(x.begin()),last(x.end()); itr!=last; ++itr) cout<< " " << *itr; cout<<endl; } }; // 汎用構造体ラッパ template <typename t_type> struct TStruct { typedef t_type T; T &Entity; TStruct(T &entity) : Entity(entity) {} }; // TStruct オブジェクトの生成関数 template <typename T> TStruct<T> Struct(T &entity) {return TStruct<T>(entity);} // 汎用構造体に対する部分特殊化 template <typename T> struct TPrintObj<TStruct<T> > { void operator()(const TStruct<T> &x) { cout<<"values are "<<endl; cout<<" .X= "<<x.Entity.X<<endl; cout<<" .Y= "<<x.Entity.Y<<endl; } }; // 適当な構造体 struct TItem { int X; double Y; }; int main(int argc, char**argv) { list<double> tmp1; tmp1.push_back(1.2); tmp1.push_back(-5.5); tmp1.push_back(0.001); TItem tmp2= {-12, 4.55}; Print(2.23); Print(-10); Print(tmp1); Print(Struct(tmp2)); return 0; }
value is 2.23 value(int) is -10 values(list) are 1.2 -5.5 0.001 values are .X= -12 .Y= 4.55
のような結果が得られる.
ところで,ここで実装した汎用な構造体に対する特殊化は,メンバとして X, Y を持つ構造体に限定される.前述したが,このような場合の素直な実装は,X, Y を持つ基底構造体 TBase を作って,TPrintObj を TBase に対して特殊化(部分ではない)しておき,TBase を派生させてその他の構造体を作る,というものだろう.
だから,このテクニックが必要になる場面は,かなり限定されることに注意されたい.例えば,対象となる構造体(やクラス)が,他人が作ったライブラリの中にあって,手が出せない場合などだ.しかしその場合でも,その特定の構造体について特殊化することを検討してみた方がいい.
さらに脱線した話
実のところ,私が開発しているプログラムでは,結局,上のような実装を用いなかった.その理由は, Struct(..) のようなコードを書くと,いくつかのケースで弊害となることが分かったからだ.個々の構造体に対して個別に特殊化するという解決策も,もちろんあり得た.しかし,特殊化したい構造体の数が未知だったので,できれば部分特殊化で実現したかった.そして,Struct(..) を用いずに解決した.それは,TPrintObj のデフォルトの operator() を構造体に対する定義としてしまう,というものだ.上記の TPrintObj では,デフォルトの operator() は既に実装されていたが,私のプログラムでは,単にエラーメッセージを出力するだけのものだった.そこで思い切って,構造体に対して書くべきコードに変更した.
まとめ
- 汎用的な構造体に対する部分特殊化を行うには,TStruct のような「構造体ラッパ」を書けばいい
- でも,本当にそんな実装にする必要があるのか,よく考えよう
- 代替案1: 個々の構造体ごとに特殊化する
- 代替案2: 特殊化したい構造体群に共通の基底構造体を作る
- 場合によっては,デフォルトの実装を,構造体に対する実装とすることができる