SFINAE
"替換失敗不是錯誤"
這條規則應用於函式模板的過載決議:當替換顯式指定的或推匯出的型別以作為模板引數時失敗,該特化將從過載集合中丟棄,而不是導致編譯錯誤。
此特性用於模板超程式設計。
目錄 |
[編輯] 解釋
函式模板引數被替換(被模板實參替換)兩次
- 顯式指定的模板實參在模板實參推導之前替換
- 推匯出的實參和從預設值獲得的實參在模板實參推導之後替換
替換髮生在
- 函式型別中使用的所有型別(包括返回型別和所有引數的型別)
- 模板引數宣告中使用的所有型別
- 部分特化的模板實參列表中使用的所有型別
|
(C++11 起) |
|
(C++20 起) |
替換失敗是指在上述型別或表示式在替換實參後是不合法的(需要診斷)任何情況。
只有在函式型別或其模板引數型別或其explicit 說明符(C++20 起)的即時上下文中的型別和表示式中發生的失敗才是 SFINAE 錯誤。如果替換型別/表示式的求值導致副作用,例如例項化某個模板特化,生成隱式定義的成員函式等,這些副作用中的錯誤將被視為硬錯誤。lambda 表示式不被視為即時上下文的一部分。(C++20 起)
本節不完整 原因:此問題相關的迷你示例 |
替換按詞法順序進行,並在遇到失敗時停止。
如果存在多個具有不同詞法順序的宣告(例如,一個函式模板使用尾隨返回型別宣告,該型別在引數之後替換;而又使用普通返回型別重新宣告,該型別將在引數之前替換),並且這會導致模板例項化以不同的順序發生或根本不發生,則程式不合法;不需要診斷。 |
(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 陣列、引用陣列、函式陣列、負大小陣列、非整型大小陣列或零大小陣列
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 應用的元函式。 |
(C++17 起) |
[編輯] 替代方案
在適用情況下,通常優先使用標籤分派、if constexpr
(C++17 起)和概念 (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++ 標準。
缺陷報告 | 應用於 | 釋出時的行為 | 正確的行為 |
---|---|---|---|
CWG 295 | C++98 | 建立 cv-qualified 函式型別 可能導致替換失敗 |
不再是失敗, 丟棄 cv-qualification |
CWG 1227 | C++98 | 替換順序未指定 | 與詞法順序相同 |
CWG 2054 | C++98 | 部分特化中的替換未正確指定 | 已指定 |
CWG 2322 | C++11 | 不同詞法順序的宣告會導致模板 例項化以不同順序發生或根本不發生 |
這種情況是不合法的, 不需要診斷 |