SFINAE
"Substitution Failure Is Not An Error"
このルールは関数テンプレートのオーバーロード解決中に適用されます: 明示的に指定された、または推論された型をテンプレートパラメータに代入する際に失敗した場合、その特殊化はコンパイルエラーを引き起こす代わりに、オーバーロードセットから破棄されます。
この機能はテンプレートメタプログラミングで利用されます。
目次 |
[編集] 説明
関数テンプレートのパラメータは2回置換(テンプレート引数で置き換え)されます。
- 明示的に指定されたテンプレート引数はテンプレート引数推論の前に置換されます。
- 推論された引数とデフォルトから取得された引数はテンプレート引数推論の後に置換されます。
置換は以下の箇所で行われます。
- 関数型で使用されるすべての型(戻り値の型とすべてのパラメータの型を含む)
- テンプレートパラメータ宣言で使用されるすべての型
- 部分特殊化のテンプレート引数リストで使用されるすべての型
|
(C++11以降) |
|
(C++20以降) |
置換失敗とは、上記の型または式が、置換された引数を使用して記述された場合に、ill-formed(診断が必要な状態)となるあらゆる状況を指します。
関数型またはそのテンプレートパラメータ型の即時コンテキスト内の型と式における失敗、またはそのexplicit指定子(C++20以降)のみがSFINAEエラーと見なされます。置換された型/式の評価が、何らかのテンプレート特殊化のインスタンス化、暗黙的に定義されたメンバ関数の生成などの副作用を引き起こす場合、それらの副作用におけるエラーはハードエラーとして扱われます。ラムダ式は即時コンテキストの一部とは見なされません。(C++20以降)
| このセクションは未完成です 理由: これが重要となるミニ例 |
置換は字句順に進み、失敗に遭遇すると停止します。
|
異なる字句順を持つ複数の宣言(例えば、パラメータの後に置換される後置戻り値型で宣言された関数テンプレートと、パラメータの前に置換される通常の戻り値型で再宣言された関数テンプレート)があり、それがテンプレートのインスタンス化を異なる順序で発生させたり、全く発生させなかったりする場合、そのプログラムはill-formedです。診断は要求されません。 |
(C++11以降) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // SFINAE failure if T has no member type class V = typename B<T>::type> // hard error if B has no member type // (guaranteed to not occur via CWG 1227 because // substitution into the default template argument // of U would fail first) void foo (int); template<class T> typename T::type h(typename B<T>::type); template<class T> auto h(typename B<T>::type) -> typename T::type; // redeclaration template<class T> void h(...) {} using R = decltype(h<int>(0)); // ill-formed, no diagnostic required
[編集] 型SFINAE
以下の型エラーはSFINAEエラーです。
|
(C++11以降) |
- voidの配列、参照の配列、関数の配列、負のサイズの配列、非整数サイズの配列、またはサイズ0の配列を作成しようとすること。
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // this overload is selected when I is even } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // this overload is selected when I is odd }
- スコープ解決演算子
::の左側で型を使用しようとして、それがクラスまたは列挙型ではない場合。
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // uses second overload
- 型のメンバーを使用しようとして、以下の場合。
- その型が指定されたメンバーを含んでいない場合。
- 指定されたメンバーが型が必要な場所で型ではない場合。
- 指定されたメンバーがテンプレートが必要な場所でテンプレートではない場合。
- 指定されたメンバーが非型が必要な場所で非型ではない場合。
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() { // Deduction fails in each of these cases: f<A>(0); // A does not contain a member Y f<B>(0); // The Y member of B is not a type g<C>(0); // The N member of C is not a non-type h<D>(0); // The TT member of D is not a template // Deduction succeeds in each of these cases: 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::*); // selected if C is a class type template<typename C> static no& test(...); // selected otherwise public: static bool const value = sizeof(test<T>(nullptr)) == 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); // can’t conv 1 to int* // todo: needs to demonstrate overload resolution, not just failure
- void型のパラメータを持つ関数型を作成しようとすること。
- 配列型または関数型を返す関数型を作成しようとすること。
[編集] 式SFINAE
|
C++11より前は、型(配列の境界など)で使用される定数式のみがSFINAEとして扱われる(ハードエラーではない)ことが要求されていました。 |
(C++11まで) |
|
以下の式エラーはSFINAEエラーです。
struct X {}; struct Y { Y(X){} }; // X is convertible to Y template<class T> auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1 X f(Y, Y); // overload #2 X x1, x2; X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed) // only #2 is in the overload set, and is called |
(C++11以降) |
[編集] 部分特殊化におけるSFINAE
クラスまたは変数(C++14以降)テンプレートの特殊化が、何らかの部分特殊化によって生成されるのか、主テンプレートによって生成されるのかを判断する際にも、推論と置換が発生します。このような判断中に置換失敗はハードエラーとして扱われず、代わりに、関数テンプレートが関与するオーバーロード解決の場合と同様に、対応する部分特殊化宣言は無視されます。
// primary template handles non-referenceable types: template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // specialization recognizes referenceable types: template<class T> struct reference_traits<T, std::void_t<T&>> { using add_lref = T&; using add_rref = T&&; }; template<class T> using add_lvalue_reference_t = typename reference_traits<T>::add_lref; template<class T> using add_rvalue_reference_t = typename reference_traits<T>::add_rref;
[編集] ライブラリサポート
|
標準ライブラリコンポーネントであるstd::enable_ifは、コンパイル時に評価される条件に基づいて特定のオーバーロードを有効または無効にするために、置換失敗を引き起こすことができます。 さらに、多くの型特性は、適切なコンパイラ拡張が利用できない場合、SFINAEを使用して実装する必要があります。 |
(C++11以降) |
|
標準ライブラリコンポーネントであるstd::void_tは、部分特殊化SFINAEのアプリケーションを簡素化するもう1つのユーティリティメタ関数です。 |
(C++17以降) |
[編集] 代替手段
適用可能な場合、タグディスパッチ、if constexpr(C++17以降)、およびコンセプト(C++20以降)は通常、SFINAEの使用よりも推奨されます。
|
条件付きコンパイル時エラーのみが必要な場合は、通常、SFINAEよりも |
(C++11以降) |
[編集] 例
一般的なイディオムは、戻り値の型に式SFINAEを使用することです。この式ではカンマ演算子が使用され、左の副式が調べられているものであり(ユーザー定義のカンマ演算子が戻り値の型で選択されないようにvoidにキャストされます)、右の副式が関数が返すことになっている型を持ちます。
#include <iostream> // This overload is added to the set of overloads if C is // a class or reference-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)(c.*f)(), void()) { std::cout << "(1) Class/class reference overload called\n"; } // This overload is added to the set of overloads if C is a // pointer-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)((c->*f)()), void()) { std::cout << "(2) Pointer overload called\n"; } // This overload is always in the set of overloads: ellipsis // parameter has the lowest ranking for overload resolution void test(...) { std::cout << "(3) Catch-all overload called\n"; } int main() { struct X { void f() {} }; X x; X& rx = x; test(x, &X::f); // (1) test(rx, &X::f); // (1), creates a copy of x test(&x, &X::f); // (2) test(42, 1337); // (3) }
出力
(1) Class/class reference overload called (1) Class/class reference overload called (2) Pointer overload called (3) Catch-all overload called
[編集] 欠陥報告
以下の動作変更を伴う欠陥報告が、以前に公開されたC++標準に遡って適用されました。
| DR | 適用対象 | 公開された動作 | 正しい動作 |
|---|---|---|---|
| CWG 295 | C++98 | cv修飾された関数型の作成 置換失敗につながる可能性があった |
失敗しないように変更された cv修飾を破棄 |
| CWG 1227 | C++98 | 置換順序が未指定だった | 字句順と同じになった |
| CWG 2054 | C++98 | 部分特殊化における置換が正しく指定されていなかった | 指定された |
| CWG 2322 | C++11 | 異なる字句順の宣言がテンプレートの インスタンス化を異なる順序で発生させたり、全く発生させなかったりする |
そのような場合はill-formedである 診断は要求されない |