Posted at

SFINAE で遊んでみた 1/2

More than 5 years have passed since last update.

#include <stdio.h>

#include <vector>
#include <map>
using namespace std;

// 準備中です...
int main() { return 0; }
struct Out {
Out() {}
Out(const Out& out) { puts(""); }
Out& operator|(const char* mes) { puts(mes); return *this; }
Out& operator|(int n) { printf("# %d // ", n); return *this; }
};
template<int> struct Holder { static Out out; };
#define O template<> Out Holder<__LINE__>::out = Out()
#define V(...) __VA_ARGS__ | #__VA_ARGS__

O | "C++ の SFINAE という仕様を利用したトリックが"
| "思った以上にパワフルだったので遊んでみました.";

O | "SFINAE とは,特定条件下でテンプレート置き換えの結果がエラーになっても,"
| "それをコンパイルエラーとせず,単にそのテンプレートを無視するという"
| "C++ の特徴です.";

O | "特定の型引数でエラーになるようなテンプレート A を書いても"
| "SFINAE によって,コンパイルエラーにならず別のテンプレート B に"
| "フォールバックさせることができるので,"
| "エラーの有無で A B を切り替えるような挙動がコンパイル時に可能です.";

O | "★ SFINAE を使わない判定器";

O | "まず,value 定数を持つ2つの型を用意します.";

struct true_v {
enum { value = 1 };
};

struct false_v {
enum { value = 0 };
};

O | "次に is_void テンプレートを用意します."
| "デフォルトの実装では false_v を継承します.";

template <class T>
struct is_void : public false_v {};

O | "false_v を継承しているので,is_void<XXX>::value は 0 ですね."
| "確認してみましょう";

O | V(is_void<double>::value);

O | "is_void はテンプレートなので"
| "特定のテンプレート引数に対して特殊化できます."
| "つまり…";

template <>
struct is_void<void> : public true_v {};

O | "特殊化することで与えられた型引数が void の時だけ true_v を継承し,"
| "value が 1 を返すような判定器になりました."
| "確認してみましょう.";

O | V(is_void<char>::value)
| V(is_void<long>::value)
| V(is_void<void>::value);

O | "これは普通のテンプレート特殊化で,SFINAE は使っていません.";

O | "★ SFINAE を使う判定器";

O | "次は有名な STL コンテナ型の判定器を作ってみます."
| "この判定器はある型 T が T::iterator を持つか否かで"
| "その型が STL コンテナであるか否かを判定します."
| "実際には型 T が STL コンテナであるためには,"
| "もっと厳しい条件がつくそうなので,これは簡易判定器です.";

O | "先ほどと同じように特殊化前のテンプレートを false_v を継承して用意します.";

template <class T, class X=void>
struct is_stl : public false_v {};

O | "is_void と異なり is の型引数に X が加わわっています."
| "特殊化する時に,この X に T::iterator を含むか否かを"
| "コンパイラに判断させる式をあてます.";

O | "次にヘルパーテンプレートを用意します.";

template <class>
struct ignore {
typedef void type;
};

O | "このテンプレートは次のように type を参照するために使います."
| "テンプレート引数が宣言内でまったく使われていないので,"
| "どんな引数を渡しても type は void になります."
| "引数を無視するという意味で ignore と名づけました."
| "本当に void になっているのか,先ほどの is_void で"
| "確認してみましょう.";

O | V(is_void< ignore<void>::type >::value)
| V(is_void< ignore<int>::type >::value)
| V(is_void< ignore< vector<int> >::type >::value);

O | "どうやらきちんと,void になっているようです."
| "最後にこのヘルパーテンプレートを使って, is_stl を特殊化します.";

template <class T>
struct is_stl<T, typename ignore<typename T::iterator>::type> :
public true_v {};

O | "第一引数はテンプレート T がそのままあてられていて特殊化されていません."
| "第二引数は ignore<XXX>::type があてられているのでコイツは void です."
| "つまりこの特殊化は,第二引数が void である場合に適用される"
| "ということになります.";

O | V(is_stl< vector<int>, int >::value)
| V(is_stl< vector<int>, void >::value)
| V(is_stl< map<int, int>, void >::value);

O | "また第二引数のデフォルトは void なので省略した場合も特殊化が行われます.";

O | V(is_stl< vector<int> >::value)
| V(is_stl< map<int, int> >::value);

O | "では 第一引数はなんでも良いのかというとそうではなく"
| "テンプレートの特殊化で T::iterator という記述がされているので"
| "iterator が存在しないような型,"
| "例えば int が T として渡されると,int::iterator エラーになります."
| "T::iterator は ignore テンプレートによって無視されてしまう部分ですが,"
| "コンパイラはきちんと解釈を行なっているのです.";

O | "ここでようやく SFINAE の登場です."
| "通常 int::iterator など存在しない識別子を参照した場合,"
| "これはコンパイラエラーになってしまいます.";

O | "しかしテンプレートの置き換えによって生じた今回のエラーは話が別で,"
| "SFINAE によってコンパイルエラーとしては扱われず,単にこの特殊化が"
| "無視されるだけですみます.";

O | "結果として T が iterator を持たない場合は特殊化の宣言がなくなり,"
| "元の宣言が使われることになります.";

O | V(is_stl< vector<int> >::value)
| V(is_stl< map<int, int> >::value)
| V(is_stl< int >::value)
| V(is_stl< double >::value);

O | "実はこのテンプレートは第二引数を常に省略して使うので,"
| "T::iterator がエラーになるかどうかが,特殊化の成否を分けます."
| "STL コンテナ判定器.完成.";