名稱空間
變體
操作

SFINAE

來自 cppreference.com
< cpp‎ | 語言
 
 
C++ 語言
 
 
 
 

"替換失敗不是錯誤"


這條規則應用於函式模板的過載決議:當替換顯式指定的或推匯出的型別以作為模板引數時失敗,該特化將從過載集合中丟棄,而不是導致編譯錯誤。

此特性用於模板超程式設計。

目錄

[編輯] 解釋

函式模板引數被替換(被模板實參替換)兩次

  • 顯式指定的模板實參在模板實參推導之前替換
  • 推匯出的實參和從預設值獲得的實參在模板實參推導之後替換

替換髮生在

  • 函式型別中使用的所有型別(包括返回型別和所有引數的型別)
  • 模板引數宣告中使用的所有型別
  • 部分特化的模板實參列表中使用的所有型別
  • 函式型別中使用的所有表示式
  • 模板引數宣告中使用的所有表示式
  • 部分特化的模板實參列表中使用的所有表示式
(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。

如果只需要條件編譯時錯誤,通常優先使用static_assert,而不是 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 不同詞法順序的宣告會導致模板
例項化以不同順序發生或根本不發生
這種情況是不合法的,
不需要診斷