SFINAE
"Substitution Failure Is Not An Error" (置換失敗はエラーではない)
このルールは関数テンプレートのオーバーロード解決中に適用されます。 テンプレート引数に対する明示的に指定されたまたは推定された型の置換が失敗したとき、コンパイルエラーが発生する代わりに、その特殊化がオーバーロード集合から取り除かれます。
この機能はテンプレートメタプログラミングで使用されます。
目次 |
[編集] 説明
関数のテンプレート仮引数の置換 (テンプレート実引数への置き換え) は2回行われます。
- 明示的に指定されたテンプレート実引数はテンプレートの実引数推定の前に置換されます。
- 推定された実引数とデフォルトから取得された実引数はテンプレートの実引数推定の後で置換されます。
置換は以下の場所で発生します。
- 関数の型で使用されたすべての型 (戻り値の型およびすべての仮引数の型を含みます)。
- テンプレート仮引数の宣言で使用されたすべての型
|
(C++11以上) |
|
(C++20以上) |
置換失敗は、もし上記の型または式が置換後の実引数を用いて書かれたならば ill-formed (診断が要求される) になるであろう、あらゆる状況を指します。
関数の型またはテンプレート仮引数の型または explicit 指定子 (C++20以上)の直接の文脈における型および式における失敗のみが SFINAE のエラーとなります。 置換後の型または式の評価が何らかのテンプレートの特殊化や暗黙に定義されたメンバ関数の生成などの副作用を発生させる場合、それらの副作用で発生したエラーはハードなエラーとして扱われます。 ラムダ式は直接の文脈の一部とみなされません。 (C++20以上)
This section is incomplete Reason: mini-example where this matters |
置換は字句的順序で行われ、失敗に遭遇したときに停止します。 template <typename A> struct B { using type = typename A::type; }; template < class T, class = typename T::type, // T が type メンバを持たない場合、 SFINAE の失敗です。 class U = typename B<T>::type // T が type メンバを持たない場合、ハードなエラーです // (これは C++14 以上では発生しないことが保証されます)。 > void foo (int); 異なる字句的順序を持つ複数の宣言が存在する場合 (例えば、後置戻り値型を持つ関数テンプレートを宣言し、その戻り値型が引数の後で置換され、その関数を引数の前に置換される通常の戻り値型で再宣言した場合など)、プログラムは ill-formed です (診断は要求されません)。 |
(C++14以上) |
[編集] 型 SFINAE
以下の型エラーは SFINAE のエラーです。
|
(C++11以上) |
- void の配列、参照の配列、関数の配列、サイズが負の配列、サイズが整数でない配列、またはサイズがゼロの配列を作成しようとした。
template <int I> void div(char(*)[I % 2 == 0] = 0) { // I が偶数のときは、このオーバーロードが選択されます。 } template <int I> void div(char(*)[I % 2 == 1] = 0) { // I が奇数のときは、このオーバーロードが選択されます。 }
- スコープ解決演算子
::
の左にクラスや列挙でない型を使用しようとした。
template <class T> int f(typename T::B*); template <class T> int f(T); int i = f<int>(0); // 2つめのオーバーロードを使用します。
- 以下の状況で、何らかの型のメンバを使用しようとした。
- その型に指定されたメンバがない。
- 型が必要な場所で、指定されたメンバが型でない。
- テンプレートが必要な場所で、指定されたメンバがテンプレートでない。
- 非型が必要な場所で、指定されたメンバが非型でない。
template <int I> struct X { }; template <template <class T> class> struct Z { }; template <class T> void f(typename T::Y*){} template <class T> void g(X<T::N>*){} template <class T> void h(Z<T::template TT>*){} struct A {}; struct B { int Y; }; struct C { typedef int N; }; struct D { typedef int TT; }; struct B1 { typedef int Y; }; struct C1 { static const int N = 0; }; struct D1 { template <typename T> struct TT { }; }; int main() { // 以下のケースはいすれも推定に失敗します。 f<A>(0); // A にメンバ Y はありません。 f<B>(0); // B のメンバ Y は型ではありません。 g<C>(0); // C のメンバ N は非型ではありません。 h<D>(0); // D のメンバ TT はテンプレートではありません。 // 以下のケースはいずれも推定に成功します。 f<B1>(0); g<C1>(0); h<D1>(0); } // todo: needs to demonstrate overload resolution, not just failure
- 参照へのポインタを作成しようとした。
- void への参照を作成しようとした。
- T のメンバポインタを作成しようとしたが、 T がクラス型でない。
template<typename T> class is_class { typedef char yes[1]; typedef char no [2]; template<typename C> static yes& test(int C::*); // C がクラス型の場合に選択されます。 template<typename C> static no& test(...); // それ以外の場合に選択されます。 public: static bool const value = sizeof(test<T>(0)) == sizeof(yes); };
- 非型テンプレート引数に無効な型を与えようとした。
template <class T, T> struct S {}; template <class T> int f(S<T, T()>*); struct X {}; int i0 = f<X>(0); // todo: needs to demonstrate overload resolution, not just failure
- 以下の場所で無効な変換を行おうとした。
- テンプレート実引数の式内。
- 関数宣言で使用される式内。
template <class T, T*> int f(int); int i2 = f<int,1>(0); // 1 を int* に変換できません。 // todo: needs to demonstrate overload resolution, not just failure
- void 型の引数を取る関数型を作成しようとした。
- 配列型または関数型を返す関数型を作成しようとした。
|
(C++11未満) |
[編集] 式 SFINAE
以下の式エラーは SFINAE のエラーです。
struct X {}; struct Y { Y(X){} }; // X は Y に変換可能です。 template <class T> auto f(T t1, T t2) -> decltype(t1 + t2); // オーバーロード #1 X f(Y, Y); // オーバーロード #2 X x1, x2; X x3 = f(x1, x2); // #1 に対する推定は失敗します (式 x1+x2 は ill-formed です)。 // #2 のみがオーバーロード集合に含まれ、それが呼ばれます。 |
(C++11以上) |
C++11 より前では、 SFINAE として扱われる (ハードなエラーにならない) ことが要求されたのは、型で使用された定数式 (配列のサイズなど) だけでした。 |
(C++11未満) |
[編集] ライブラリサポート
標準ライブラリのコンポーネント std::enable_if は、コンパイル時に評価される条件に基づいて特定のオーバーロードを有効化または無効化するための置換失敗を作成する、ユーティリティメタ関数です。
標準ライブラリのコンポーネント std::void_t は、 SFINAE の適用を単純化する、もうひとつのユーティリティメタ関数です。
また、多くの型特性が SFINAE を用いて実装されています。
[編集] 代替手法
適用可能であれば、タグディスパッチ、 static_assert、およびコンセプト (利用可能な場合) の方が、 SFINAE を直接使うよりも、通常は好まれます。
[編集] 例
よくあるイディオムは戻り値の型で式 SFINAE を使うことです。 コンマ演算子を使用し、その左側には調べたい式を (その戻り値に対するユーザ定義コンマ演算子が選択されないように void にキャストします)、右側にはその関数が返したい型の式を記述します。
#include <iostream> // このオーバーロードは常にオーバーロード集合に含まれます。 // 省略記号により、オーバーロード解決において最も低い順位となります。 void test(...) { std::cout << "Catch-all overload called\n"; } // このオーバーロードは、 C がクラスへの参照型であり、 // F が C のメンバ関数ポインタである場合に、オーバーロード集合に含まれます。 template <class C, class F> auto test(C c, F f) -> decltype((void)(c.*f)(), void()) { std::cout << "Reference overload called\n"; } // このオーバーロードは、 C がクラスへのポインタ型であり、 // F が C のメンバ関数ポインタである場合に、オーバーロード集合に含まれます。 template <class C, class F> auto test(C c, F f) -> decltype((void)((c->*f)()), void()) { std::cout << "Pointer overload called\n"; } struct X { void f() {} }; int main(){ X x; test( x, &X::f); test(&x, &X::f); test(42, 1337); }
出力:
Reference overload called Pointer overload called Catch-all overload called