SFINAE
「置換失敗はエラーではない」
このルールは関数テンプレートのオーバーロード解決中に適用されます:
substituting
明示的に指定された、または
deduced type
をテンプレートパラメータに
置換
する際に失敗した場合、その特殊化はコンパイルエラーを引き起こす代わりに
overload set
から除外されます。
この機能はテンプレートメタプログラミングで使用されます。
目次 |
説明
関数テンプレートのパラメータは二回置換(テンプレート引数による置き換え)されます:
- 明示的に指定されたテンプレート引数は、テンプレート引数推論の前に置換される
- 推論された引数とデフォルトから取得された引数は、テンプレート引数推論の後に置換される
置換は以下で発生します
- 関数型で使用されるすべての型(戻り値の型とすべてのパラメータの型を含む)
- テンプレートパラメータ宣言で使用されるすべての型
- 部分特殊化のテンプレート引数リストで使用されるすべての型
|
(C++11以降) |
|
(C++20以降) |
置換失敗 とは、置換された引数を使用して記述された場合に、上記の型または式が不適格(診断メッセージが要求される)となる状況を指します。
関数の型、そのテンプレートパラメータの型 またはその explicit specifier (since C++20) の 直接のコンテキスト における型と式の失敗のみがSFINAEエラーとなります。置換された型/式の評価が、何らかのテンプレート特殊化のインスタンス化、暗黙的に定義されるメンバ関数の生成などの副作用を引き起こす場合、それらの副作用におけるエラーはハードエラーとして扱われます。 lambda expression は直接のコンテキストの一部とは見なされません。 (since C++20)
|
このセクションは不完全です
理由:重要性を示すミニ例 |
置換は字句順に進行し、失敗が検出された時点で停止します。
|
異なる字句順序を持つ複数の宣言(例えば、パラメータの後に置換される後置戻り値型で宣言された関数テンプレートと、パラメータの前に置換される通常の戻り値型で再宣言されたもの)が存在し、それによってテンプレートのインスタンス化が異なる順序で発生する、あるいは全く発生しなくなる場合、そのプログラムは不適格である。診断は不要。 |
(C++11以降) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // Tがメンバ型typeを持たない場合のSFINAE失敗 class V = typename B<T>::type> // Bがメンバ型typeを持たない場合のハードエラー // (CWG 1227により、Uのデフォルトテンプレート引数への // 置換が先に失敗するため発生しないことが保証される) 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; // 再宣言 template<class T> void h(...) {} using R = decltype(h<int>(0)); // 不適格、診断不要
型SFINAE
以下の型エラーはSFINAEエラーです:
|
(C++11以降) |
- voidの配列、参照の配列、関数の配列、負のサイズの配列、非整数サイズの配列、またはサイズゼロの配列を作成しようとする場合:
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // このオーバーロードはIが偶数の場合に選択される } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // このオーバーロードは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: オーバーロード解決のデモンストレーションが必要(単なる失敗だけでなく)
- 参照へのポインタの作成を試行中
- 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>(nullptr)) == sizeof(yes); };
- 定数テンプレートパラメータに無効な型を指定しようとした場合:
template<class T, T> struct S {}; template<class T> int f(S<T, T()>*); struct X {}; int i0 = f<X>(0); // 未実装: 単なる失敗ではなく、オーバーロード解決の実証が必要
- 無効な変換を実行しようとしています
-
- テンプレート引数式内で
- 関数宣言で使用される式内で:
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // 1をint*に変換できない // todo: オーバーロード解決の実例が必要、単なる失敗例だけでなく
- void型のパラメータを持つ関数型を作成しようとしています
- 配列型または関数型を返す関数型を作成しようとしています
式 SFINAE
|
C++11以前では、型(配列の境界など)で使用される定数式のみがSFINAE(置き換え失敗はエラーではない)として扱われる必要がありました。 |
(until 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 |
(since C++11) |
SFINAE の部分特殊化における適用
クラステンプレート または変数 (C++14以降) の特殊化が 部分特殊化 またはプライマリテンプレートによって生成されるかどうかを決定する際にも、deductionとsubstitutionが発生します。この決定過程におけるsubstitution failureはハードエラーとして扱われず、関数テンプレートのオーバーロード解決と同様に、対応する部分特殊化の宣言が無視されます。
// プライマリテンプレートは参照不可能な型を処理する: template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // 特殊化は参照可能な型を認識する: 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 は、コンパイル時に評価される条件に基づいて特定のオーバーロードを有効または無効にするために、置換失敗を発生させることを可能にします。 さらに、多くの type traits は、適切なコンパイラ拡張が利用できない場合、SFINAEを使用して実装されなければなりません。 |
(C++11以降) |
|
標準ライブラリのコンポーネント std::void_t は、部分特殊化SFINAEアプリケーションを簡素化するもう一つのユーティリティメタ関数です。 |
(C++17以降) |
代替案
該当する場合、
tag dispatch
、
if constexpr
(C++17以降)
および
concepts
(C++20以降)
は通常、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 |
異なる字句順での宣言により、テンプレートの
インスタンス化が異なる順序で発生する、または全く発生しない |
そのような場合は不適格、
診断は要求されない |