我們之前已經介紹過 SFINAE 及型別推導失敗的處理. 在這篇文章中, 我們會綜合地對這些知識進行運用
我們首先要解決的問題是可推導語境的問題, 我將使用《【C++ Template Meta-Programming】函式多載與 SFINAE 初步》文章中的實例 :
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
的型別. 暫時拋開這個實例, 我們首先實作 C++ 標準樣板程式庫中的超函式 std::enable_if
. std::enable_if
接受兩個樣板引數, 第一個樣板引數是布林型別, 第二個樣板引數由使用者來決定. 通過引數的類別和這個樣板的名稱, 我們大致可以猜測到這個樣板的用處 : 若且唯若第一個樣板引數為 true
的時候, 開啟; 否則, 關閉. 開啟是指它被實作, 而關閉則是指它未被實作. 這樣, 通過未實作的型別, 它可以被用於 SFNIAE, 使得某些函式從多載函式候選集合中移除. 結合第二個樣板參數, 我們又可以猜到在開啟的情況下, std::enable_if
的回傳結果將會是第二個樣板引數; 在關閉的情況下, std::enable_if
僅僅被宣告, 未被實作 :
template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
結合 SFINAE 替換失敗的情況, 當函式引數對應的型別中, 出現未實作的類別, 替換將會失敗. 也就是說, 一旦函式引數的型別為 typename enable_if<false, T>::type
, 由於它未被實作, 所以替換失敗. 現在讓我們回到第一個實例, 把其中的 T
替換成 enable_if
. 不妨假設第一個引數是 is_int
的回傳結果, 用於判定 T
是否為一個 int
型別 :
template <typename T>
struct is_int {
constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<volatile int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const volatile int> {
constexpr static inline auto value {true};
};
對於 inline
的使用, 參見文章《《Effective C++》讀後感 – 從自己的角度講一講《Effective C++》》條款 30
template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
template <typename T>
struct is_int {
constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<volatile int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const volatile int> {
constexpr static inline auto value {true};
};
template <typename T>
void f_if_integral(typename enable_if<is_int<T>::value, T>::type) {}
int main(int argc, char *argv[]) {
f_if_integral(0); //Error
}
顯而易見, 和第一個實例一樣, 上述程式碼存在編碼錯誤. 和第一個實例不同的只是將第一個實例中的函式 f
的引數 typename T::type
中的 T
替換成了 enable_if<is_int<T>::value, T>
罷了. 但是如果你閱讀過 C++ 標準樣板程式庫的程式碼, 你就會發現, std::vector
的建構子接受一個範圍的疊代器, 這個範圍的疊代器對應的參數型別使用了 std::enable_if
. 很顯然, 這個建構子是可以正常運作的. 因此, 你肯定希望上述程式碼也正常運作. 現在我們來探究 std::vector
接受疊代器的建構子, 它大概長這個樣子 :
template <typename InputIterator>
vector::vector(typename enable_if<is_input_iterator<InputIterator>::value, InputIterator>::type, InputIterator);
這個是建構子的宣告, 和之前出現編碼錯誤的函式 f
及 func
不同的是, vector
的建構子有兩個參數. 為什麼多了一個參數, 就不會出現編碼錯誤了呢? 這是因為多了一個參數之後, 編碼器就可以確定 InputIterator
的具體型別了, 這個型別是從給定的第二個引數對應的型別推導的. 既然知道了 InputIterator
的具體型別, 那麼自然也就可以確定超函式 enable_if
所回傳的 InputIterator
是什麼型別. 簡化一下這個實例, 也就是 :
template <typename T>
void f(typename T::type, T) {}
如果這樣寫, 編碼器從給定的第二個引數中知道了 T
的型別, 自然也就可以知道 T
中是否存在一個名為 type
的型別, 然後進行函式匹配. 我們稱這樣的語境為可推導語境 (deduced context). 而類似第一個實例中的語境, 我們稱之為不可推導語境 (non-deduced context)
在第一個實例中, 函式 func
確實只有一個參數, 而可推導語境至少需要兩個參數. 解決的方案也非常簡單, 強行給函式 func
增加一個參數, 但是這個參數我們不使用就可以了 :
template <typename T>
void func(typename T::type, T);
struct Foo {
using type = int;
};
int main(int argc, char *argv[]) {
func(0, Foo()); //OK
}
但是某些類別的建構成本非常高, 所以我們希望使用內建型別作為第二個參數. 即能能夠和 T
有關, 又能夠和內建型別有關, 首先想到的就是指標 :
template <typename T>
void func(typename T::type, T *);
struct Foo {
using type = int;
};
int main(int argc, char *argv[]) {
func(0, static_cast<Foo *>(nullptr)); //OK
}
對於函式 func
, 至多也就到這裡了, 你無法省略 Foo
, 因為你需要明確告訴編碼器 T
的型別. 或者按照之前文章中所說的, 明確樣板引數的方式也可以 :
template <typename T>
void func(typename T::type);
struct Foo {
using type = int;
};
int main(int argc, char *argv[]) {
func<Foo>(0); //OK
}
但是對於之前所寫的函式 f_if_integral
, 情況就有所不同了. 我們可以增加一個參數, 並且給定一個預設引數來避免明確樣板引數的情形 :
template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
template <typename T>
struct is_int {
constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<volatile int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const volatile int> {
constexpr static inline auto value {true};
};
template <typename T>
void f_if_integral(T, typename enable_if<is_int<T>::value, void *>::type = nullptr) {}
int main(int argc, char *argv[]) {
f_if_integral(0); //OK
}
在這個實例中, 我們將 enable_if
放在了第二個樣板參數, 並且在 T
是 int
型別的情況下, 超函式 enable_if
的回傳型別為 void *
. 除此之外, 我們還給定了一個值為 nullptr
的預設引數. 這個實例也很好理解, T
的型別從函式呼叫給定的引數 0
中可以推導 (T = int
), 然後第二個引數用於 SFINAE 的替換. 當替換成功, 那麼函式匹配; 否則, 去匹配其它函式或者擲出編碼錯誤
上面所有的實例, 我們都將 SFINAE 用在了函式參數當中. 除此之外, SFINAE 還可以用在樣板的引數當中 :
#include <iostream>
using std::cout;
using std::endl;
template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
template <typename T>
struct is_int {
constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<volatile int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const volatile int> {
constexpr static inline auto value {true};
};
template <typename T, typename = typename enable_if<is_int<T>::value, void>::type>
void f_if_integral(T) {
cout << "T is integral" << endl;
}
template <typename T>
void f_if_integral(T, typename enable_if<not is_int<T>::value, void *>::type = nullptr) {
cout << "T is not integral" << endl;
}
int main(int argc, char *argv[]) {
f_if_integral(0); //輸出 : T is integral
f_if_integral(0.0f); //輸出 : T is not integral
}
在這個實例中, 我們將 enable_if
放在了樣板當中, 作為一個預設樣板引數. T
還是從函式呼叫時給定的引數來推導, 第二個樣板參數除非給定, 否則使用 enable_if
, 當 enable_if
開啟失敗, 也就是遇到了 enable_if<false, void>
的情況的時候, 型別替換失敗, 因為 enable_if<false, void>
沒有被實作, 所以它裡面並不存在一個名稱為 type
的型別別名, 這個時候編碼器會放棄這個函式的匹配
看到這裡, 你可能會說, 如果我在呼叫函式 f_if_integral
的時候, 是這樣呼叫的 :
f_if_integral<float, void>(0.0f);
或者
f_if_integral(0, nullptr);
顯然可能導致錯誤, 此時我們可以將 f_if_integral
作為介面, 將真正的實作放在 f_if_integral_impl
中 :
#include <iostream>
using std::cout;
using std::endl;
template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
using type = T;
};
template <typename T>
struct is_int {
constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<volatile int> {
constexpr static inline auto value {true};
};
template <>
struct is_int<const volatile int> {
constexpr static inline auto value {true};
};
template <typename T, typename = typename enable_if<is_int<T>::value, void>::type>
void f_if_integral_impl(T value) {
cout << "T is integral" << endl;
}
template <typename T>
void f_if_integral_impl(T, typename enable_if<not is_int<T>::value, void *>::type = nullptr) {
cout << "T is not integral" << endl;
}
template <typename T>
void f_if_integral(T value) {
f_if_integral_impl(value);
}
int main(int argc, char *argv[]) {
f_if_integral(0); //輸出 : T is integral
f_if_integral(0.0f); //輸出 : T is not integral
}
這樣就避免了使用者作死的情形
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<Args...>::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
的型別別名
看到這裡, 你可能有了一個想法, 是否可以通過 SFINAE 來控制類別的繼承. 有這個想法說明你對 C++ 有了一定的感覺, 但是很遺憾, 這個想法行不通, 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'
}
在無法看到 T
中存在一個名為 type
的型別別名的情況下, 編碼器會毫不猶豫地擲出編碼錯誤, 而並非捨棄繼承
看完了這篇文章, 你應該可以明白, 我為什麼給這篇文章取名叫作 "到處 SFINAE" 了
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :