更多 C++ 習語/SFINAE
從一組過載函式中刪除那些無法生成有效模板例項化的函式。
Substitution Failure Is Not An Error(替換失敗並非錯誤)
嚴格來說,SFINAE 是一個語言特性,而不是一個習語。然而,這個語言特性在使用 enable-if 時被以非常習慣性的方式利用。
在模板引數推導過程中,C++ 編譯器會嘗試例項化多個候選過載函式的簽名,以確保只有一個過載函式可以完美匹配給定的函式呼叫。如果在函式模板例項化期間形成了無效的引數或返回值型別,則該例項化將從過載解析集中刪除,而不是導致編譯錯誤。只要只有一個函式可以呼叫,編譯器就不會發出任何錯誤。
例如,考慮一個簡單的函式 multiply 及其模板化對應物。
long multiply(int i, int j) { return i * j; }
template <class T>
typename T::multiplication_result multiply(T t1, T t2)
{
return t1 * t2;
}
int main(void)
{
multiply(4,5);
}
在 main 中呼叫函式 multiply 會導致編譯器嘗試例項化模板化函式的簽名,即使第一個 multiply 函式是更好的匹配。在例項化期間,會產生一個無效型別:int::multiplication_result。然而,由於 SFINAE,這種無效例項化會自動被忽略。最終,只有一個 multiply 函式可以被呼叫,即第一個函式。因此編譯成功。
SFINAE 通常被用來在編譯時確定型別的屬性。例如,考慮以下 is_pointer 元函式,它在編譯時確定給定型別是否為某種型別的指標。
template <class T>
struct is_pointer
{
template <class U>
static char is_ptr(U *);
template <class X, class Y>
static char is_ptr(Y X::*);
template <class U>
static char is_ptr(U (*)());
static double is_ptr(...);
static T t;
enum { value = sizeof(is_ptr(t)) == sizeof(char) };
};
struct Foo {
int bar;
};
int main(void)
{
typedef int * IntPtr;
typedef int Foo::* FooMemberPtr;
typedef int (*FuncPtr)();
printf("%d\n",is_pointer<IntPtr>::value); // prints 1
printf("%d\n",is_pointer<FooMemberPtr>::value); // prints 1
printf("%d\n",is_pointer<FuncPtr>::value); // prints 1
}
上面的 is_pointer 元函式如果沒有 SFINAE 就不會起作用。它定義了 4 個過載的 is_ptr 函式,其中 3 個是模板,每個模板都接受一個引數:指向變數的指標、指向成員變數的指標或簡單的函式指標。所有三個函式都返回一個 char,這是故意的。最後一個 is_ptr 函式是一個通配函式,使用省略號作為引數。但是,這個函式返回一個 double,它的大小始終大於字元。
當 is_pointer 傳遞一個實際上是指標的型別(例如,IntPtr)時,由於兩個 sizeof 表示式的比較,value 會被初始化為 true。第一個 sizeof 表示式呼叫 is_ptr。如果它是一個指標,只有一個過載的模板函式匹配,而不是其他函式。但是,由於 SFINAE,不會引發錯誤,因為至少找到了一個合適的函式。如果沒有任何函式適合,則會使用帶有省略號的函式。但是,該函式返回一個 double,它大於字元,因此 sizeof 比較失敗,value 被初始化為 false。
請注意,所有 is_ptr 函式都沒有定義。只有宣告足以觸發編譯器中的 SFINAE 規則。但是,這些函式本身必須是模板。也就是說,具有常規函式的類模板將不會參與 SFINAE。參與 SFINAE 的函式必須是模板。