我們之前已經介紹過 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);

這個是建構子的宣告, 和之前出現編碼錯誤的函式 ffunc 不同的是, 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 放在了第二個樣板參數, 並且在 Tint 型別的情況下, 超函式 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...>::typevoid 本身就是一個型別, 所以預設會匹配到 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" 了