通常template metaprogramming(以下TMP)では型を受け取ってその情報を取得したり操作したりしますが,オブジェクトまたは式のみを受け取ってそのオブジェクトまたは式の型の情報を取得する方法も存在します.
例えば以下のような関数のオーバーロードを宣言します.
char (&f(char))[1]; // (A) char (&f(int))[2]; // (B)
各々「引数としてcharを受け取りchar[1]への参照を返す関数f」,「引数としてintを受け取りchar[2]への参照を返す関数f」の宣言です.これらはcharのオブジェクトを引数とするかintのオブジェクトを引数とするかで異なる関数を呼び出すことになります.
char c; int i; f(c); // (A)を呼ぶ f(i); // (B)を呼ぶ
ここで各々の呼び出しに対してsizeof演算子を適用します.
sizeof( f(c) ); // コンパイル時整数1が取得できる sizeof( f(i) ); // コンパイル時整数2が取得できる
charに対するsizeofが1であること,配列型T[N]に対するsizeofがsizeof(T) * Nであること,参照型に対するsizeofが参照されている型に対するsizeofの結果と一致すること,以上から各々のオーバーロードに対して任意の自然数を割り当てることが可能になります.ちなみに,わざわざ戻り値として参照型を指定するのは「関数の戻り値に配列型への参照は指定できるが配列型は指定できない」という制限を避けるためです.
char (&f(char))[12345]; char (&f(int))[54321]; char c; int i; sizeof( f(c) ); // コンパイル時整数12345を得る sizeof( f(i) ); // コンパイル時整数54321を得る
上の例ではオブジェクトがcharであるかintであるかをTMPの段階で識別し,対応するコンパイル時決定整数を任意に生成させています.
以上のように,関数のオーバーロードに対してsizeofを用いてコンパイル時整数を対応させ,オブジェクトもしくは式のみからその型の情報を取得する手法をsizeofハックとここでは呼ぶことにします.("sizeof hack"という言葉はすでに多くの場面で使われている言葉ですが,まだ一般的ではないと思うので「呼ぶことにする」と表現しています)
このsizeofハックはコンパイラのオーバーロード解決能力をほぼそのままTMPの領域に借用する手法と見ることができます.逆に,コンパイラのオーバーロード解決能力とほぼ同等の型情報識別能力を有した強力なTMPの手法ともいえます.
以下にこの手法の応用例をいくつか示します.
変換可能性判定
ある型Tが型Uに変換可能かどうかをTMPで判定したいとします.
この手法を説明するためにオーバーロード解決の規則のおさらいをTC++PLの7.3から抜粋します.
- 正確に一致する.つまり,変換を一切行わず,あるいはわずかな変換(たとえば,配列名からポインタ,関数名から関数ポインタ,Tからconst Tなど)だけで一致するかどうか.
- 格上げによって一致する.つまり,整数の格上げ(boolからint,charからint,shortからintとそのunsigned版),floatからdouble,doubleからlong doubleへの格上げによって一致するかどうか.
- 標準変換(たとえば,intからdouble,doubleからint,Derived*からBase*,T*からvoid*,intからunsigned int)によって一致するかどうか.
- ユーザー定義変換によって一致するかどうか.
- 関数宣言に...を追加したら一致するかどうか.
このうちTがUに変換可能であるとは,型Uを引数に取る関数が上の1から4の基準によってTを引数とする呼び出しによって呼び出されることです.
template<class T, class U> struct is_convertible { char (&f(U))[1]; // (A) char (&f(...))[2]; // (B) static const bool value = (sizeof( f(t) ) == 1); // (C) private: T t; };
上のコードの(C)におけるf(t)の呼び出しは,TがUに変換可能ならば(A)を呼び,それ以外では(B)を呼びます.これによって型の間の変換可能性をTMPで判定可能です.(メンバ変数を用いている理由はここの最後の段落参照.)
Boostにおいてはboost::is_convertibleがこれと似た手法をとっています.基本的なアイデア,つまり関数のオーバーロードの識別をsizeofを通じて利用する点は変わりません.
自由関数(free-standing function)存在判定
メンバ関数の存在判定についてはSFINAEを用いた別の手法が存在するためそれはまた別の機会に扱います.
以下のコードはstd::ostreamのオブジェクトosと型Tのオブジェクトtに対して os << tという式が宣言されているかどうかをTMPで判定するコードです.
struct anything { template<class T> anything(T const &); }; struct not_printable_impl { char (&operator,(char))[2]; }; not_printable_impl operator<<(anything, anything); template<class T> struct is_printable { static const bool value = (sizeof( (os << t), 'x') == 2); private: T t; std::ostream &os; };
オブジェクト・式の型取得(typeofエミュレーション)
sizeofハックによりオブジェクトまたは式の型に基づいた整数を割り当てることができます.これを推し進めると究極的には各々の型に対して固有の整数を割り当てることが可能になります.さらに,その個々の整数を受け取って型を返すメタ関数を用意すれば,オブジェクトや式の型を受け取ってその型を返す機構,即ちtypeofのエミュレーションが可能になります.
型に固有の整数を対応させることを登録(registration),オブジェクトから対応する整数を取得する操作をエンコード(encode),その整数から元の型を取り出すことをデコード(decode)と呼ぶことにします.
template<std::size_t ID> struct decode_impl; #define REGISTER_TYPE(TYPE, ID) \ \ char (&encode_impl(TYPE))[ID]; \ \ template<> \ struct decode_impl<ID> \ { \ typedef TYPE type; \ }; #define TYPEOF(EXPR) decode_impl< sizeof( EXPR ) >::type
以上の話はtypeofエミュレーションの実際の実装の一部です.実際には上記に加えて複合型の自動推論や固有整数の生成の話などが加わりますがそれはまた別の機会に.