摘要訊息 : SFINAE 能被用在什麼地方?
0. 前言
在《【C++ Template Meta-Programming】SFINAE》文章中, 我們介紹了 SFINAE 以及可能會導致 SFINAE 替換失敗的地方. 本篇文章將綜合地對 SFINAE 進行應用.
更新紀錄 :
- 2022 年 6 月 2 日進行第一次更新和修正.
1. 可推導語境
《【C++ Template Meta-Programming】函式多載與 SFINAE 初步》文章 Code 6 中的一個實例 :
struct Foo {
using type = int;
};
template <typename T>
void func(typename T::type) {}
int main(int argc, char *argv[]) {
func(0); // no matching function for call to 'func'
}
我們曾經說過, 這個實例產生編碼錯誤的原因是因為編碼器無法確定函式樣板中T
的型別. 那麼如何修改函式 func
使得 Code 1 可以通過編碼呢? 這裡需要借用 std::enable_if
. std::enable_if
接受兩個樣板引數, 第一個樣板引數是布林型別, 第二個樣板引數由使用者來決定, 但是有一個預設樣板引數, 為 void
. 通過引數的類別和這個樣板的名稱, 我們大致可以猜測到這個樣板的用處 : 若且唯若第一個樣板引數為true
的時候, 開啟; 否則, 關閉. 開啟是指它被實作, 而關閉則是指它未被實作. 這樣, 通過未實作的型別, 它可以被用於 SFNIAE, 使得某些函式從多載函式候選集合中移除. 結合第二個樣板參數, 我們又可以猜到在開啟的情況下,std::enable_if
的回傳結果將會是第二個樣板引數; 在關閉的情況下,std::enable_if
僅僅被宣告, 未被實作 :
template <bool, typename = void>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
如果我們只希望 Code 1 中的函式 func
只接受 int
型別的引數, 那麼可能需要這樣寫 :
#include <type_traits>
struct Foo {
using type = int;
};
template <bool, typename = void>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
template <typename T>
void func(typename enable_if<std::is_same<T, int>, int>::type) {}
int main(int argc, char *argv[]) {
func(0); // no matching function for call to 'func'
}
然而 Code 3 仍然存在編碼器錯誤, 並且和 Code 1 是一樣的編碼錯誤. 如果你閱讀過 C++ 標準樣板程式庫的程式碼, 你就會發現std::vector
的建構子接受一個範圍的疊代器, 這個範圍的疊代器對應的參數型別使用了std::enable_if
. 很顯然, 這個建構子是可以正常運作的. 因此, 我們也希望 Code 3 可以正常運作. 現在我們來探究std::vector
接受疊代器的建構子, 它大概長這個樣子 :
template <typename InputIterator>
vector::vector(typename enable_if<is_input_iterator<InputIterator>::value, InputIterator>::type, InputIterator);
這個建構子的參數和函式 func
的參數唯一不同的是, vector::vector
多了一個 InputIterator
的參數, 那麼我們嘗試一下為 func
增加一個 T
:
template <typename T>
void func(typename T::type, T) {}
結果發現 func(0, Foo {});
可以通過編碼了. 問題就在第二個參數之上. 對於 void func(typename T::type);
這個宣告, 雖然我們希望編碼器可以通過尋找所有可見的類別, 找到 Foo
作為 T
, 然後令 typename T::type
替換為 int
, 使得 func(0);
可以通過編碼, 但是很遺憾這條路行不通. 因為既然 T
可以被替換為 Foo
, 萬一有一個 Bar
, 其也存在一個名稱為 type
的型別別名成員呢? 那編碼器應該用 Foo
還是 Bar
呢? 很顯然, 編碼器猜不准, 因此只能不猜, 從而產生了編碼錯誤. 對於 void func(typename T::type, T);
來說, 編碼器可以從第二個引數 Foo {}
去推導 T
, 從而得知 typename T::type
是 int
. 這樣的情況下, 不需要編碼器去猜測 T
的型別.
像 void func(typename T::type)
這樣的語境, 在 C++ 中被稱為不可推導語境 (non-deduced context), 而像 void func(typename T::type, T)
這樣的語境就是可推導語境 (deduced context). 在不可推導語境下, 函式的呼叫需要手動明確樣板參數的型別, 例如 func<Foo>(0);
這樣的程式碼就可以通過編碼.
2. 在參數中進行 SFINAE
現在我們知道, 對於類似於 Code 4 這樣的函式, 可以通過真正的原因就在於樣板參數型別是可推導的. 在這種語境下, 我們就可以通過 SFINAE 來限制函式所接受的引數的種類. 例如, 我希望某個函式只接受整型型別的引數, 那麼就可以借助 std::is_integral
先判斷給定的引數型別是否為整型型別, 如果是就讓 std::enable_if
回傳這個型別; 否則, 就讓 std::enable_if
產生替換失敗的情形 :
#include <type_traits>
template <typename T>
void f(T, typename std::enable_if<std::is_integral<T>::value, T>::type);
如果函式 f
只接受一個參數, 那麼應該怎麼辦呢? 有兩種解決方案, 一個是仍然把 SFINAE 產生的最終型別替換為指標放入函式參數當中, 另一種是把 SFINAE 產生的最終型別替換為指標放入樣板參數中 :
#include <type_traits>
template <typename T>
void f(T, typename std::enable_if<std::is_integral<T>::value, T>::type * = nullptr);
template <typename T, typename std::enable_if<std::is_integral<T>::value>::type * = nullptr>
void f(T);
這樣, 就可以通過 f(0);
這樣的形式去呼叫了.
3. 在特製化時進行 SFINAE
如果有一個類別樣板希望樣板參數 T
中的型別成員 typename T::type
進行特製化, 那麼也需要用到 SFINAE :
#include <iostream>
using std::cout;
using std::endl;
template <typename ...>
struct make_void {
using type = void;
};
template <typename T, typename = void>
struct has_type_member {
has_type_member() {
cout << "T doesn't has a member named type" << endl;
}
};
template <typename T>
struct has_type_member<T, typename make_void<typename T::type>::type> {
has_type_member() {
cout << "T has a member named type" << endl;
}
};
struct Foo {
using type = char [][3];
};
struct Bar {
using type1 = char;
};
struct type_private {
private:
using type = int;
};
int main(int argc, char *argv[]) {
has_type_member<Foo> check1; //輸出 : T has a member named type
has_type_member<Bar> check2; //輸出 : T doesn't has a member named type
has_type_member<type_private> check3; //輸出 : T doesn't has a member named type
}
首先對於超函式make_void
, 給定了任何型別我們都讓回傳型別為void
. 再來看has_type_member
, 它是一個類別樣板, 第二個樣板參數給定了一個預設樣板引數void
. 當第二個樣板引數沒有指定的時候, 預設為void
, 而typename make_void<typename T::type>::type
是has_type_member
的偏特製化, 由於超函式make_void
回傳的型別typename make_void<...>::type
和void
本身就是一個型別, 所以預設會匹配到has_type_member<T, void>
上. 但是這個匹配是有條件的, 因為void
並不是直接的void
, 而是一個位於類別make_void
中的型別別名:typename make_void<typename T::type>::type
. 一旦T
中不存在一個名為type
的型別別名, 那麼typename T::type
就會失敗, 從而導致偏特製化並不能啟用. 因此, 只能啟用沒有偏特製化的那個版本, 也就是預設的版本. 對於type_private
, 雖然類別中存在一個type
的型別別名, 但是外界並無法訪問, 所以也相當於對外界來說, 不存在一個名為type
的型別別名.
4. 使用 SFINAE 對繼承進行控制
template <typename ...>
struct make_void {
using type = void;
};
template <typename T>
struct Foo;
template <>
struct Foo<void> {};
template <typename T>
struct Bar : Foo<typename make_void<typename T::type>::type> {};
struct has_type {
using type = int;
};
struct has_no_type {};
int main(int argc, char *argv[]) {
Bar<has_type> b; // OK
Bar<has_no_type> b2; // Error : no type named 'type' in 'has_no_type'
}
很遺憾, SFINAE 並不能對繼承進行控制. 在 Code 9 中, 一旦無法看到T
中存在一個名為type
的型別別名的情況下, 編碼器會毫不猶豫地擲出編碼錯誤, 而並非捨棄繼承.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :