SFINAE, 一般用於函式樣板的多載決議中, 當函式參數被替換為實際的引數或著推導型別失敗的時候, 這個函式會從多載集合中被遺棄

在上一篇《【C++ Template Meta-Programming】函式多載與 SFINAE 初步》文章中, 我們講述了其中一種替換失敗的示例, 在本篇文章中, 我們將把剩餘的替換失敗的示例呈現出來, 並且將 SFINAE 運用到實際的編碼中

與型別相關的 SFINAE

  • 如若嘗試建立一個 void [] 型別的陣列、參考的陣列、函式的陣列、void 型別的參考、指向參考的指標、參數列表含有 void 參數的函式型別、回傳陣列型別或者函式型別的函式和回傳型別或者參數列別中含有抽象類別的函式, 那麼將會產生推導失敗
  • 如若嘗試建立一個負數大小的陣列、非整型型別的陣列和大小為 0 的陣列
#include <iostream>



using namespace std;



template <size_t N>

void func(int [N % 2 == 0 ? N : 0 /* 此處的 0 可以改成負數或著非整型 */ ] = nullptr) {

    cout << "even" << endl;

}

template <size_t N>

void func(char [N % 2 == 1 ? N : 0 /* 此處的 0 可以改成負數或著非整型 */ ] = nullptr) {

    cout << "odd" << endl;

}



int main(int argc, char *argv[]) {

    func<2>();      //輸出結果 : even

    func<3>();      //輸出結果 : odd

}
  • 如若試圖對非類別型別或者非列舉型別使用作用範圍運算子 "::"
#include <iostream>



using namespace std;



struct Foo {

    using type = int;

};


template <typename T>

void func(typename T::type) {

    cout << "T is class or enum" << endl;

}

template <typename T>

void func(T) {

    cout << "T isn't class or enum" << endl;

}



int main(int argc, char *argv[]) {

    func<char>(0);      //輸出結果 : T isn't class or enum

    func<Foo>(0);       //輸出結果 : T is class or enum

}
  • 試圖使用某一個不存在的成員型別
struct Foo {};



template <typename T>

void func(typename T::type);



int main(int argc, char *argv[]) {

    func<Foo>(0);       //candidate template ignored: substitution failure [with T = Foo]: no type named 'type' in 'Foo'

}
  • 試圖將某個不是型別的成員作為型別使用
struct Foo {

    static int i;

};

int Foo::i {0};



template <typename T>

void func(typename T::i);



int main(int argc, char *argv[]) {

    func<Foo>(0);       //candidate template ignored: substitution failure [with T = Foo]: typename specifier refers to non-type member 'i' in 
'Foo'

}
  • 試圖在樣板處使用非樣板的型別
template <template <typename> class>

class Foo {};



template <typename T>

void func(Foo<T::template T> * = nullptr);



struct Bar {

    template <typename U>

    struct T {};

};

struct Baz {

    using T = int;

};



int main(int argc, char *argv[]) {

    func<Bar>();        //正確

    func<Baz>();        //candidate template ignored: substitution failure [with T = Baz]: 'T' following the 'template' keyword does not refer to a template

}
  • 要求非型別處給定了型別
template <int N>

struct Foo {};

struct Bar {

    using num = int;

};

template <typename T>

void func(Foo<T::num> * = nullptr) {}



int main(int argc, char *argv[]) {

    func<Bar>();        //candidate template ignored: substitution failure [with T = Bar]: missing 'typename' prior to dependent type name 'Bar::num'

}
  • 試圖建立指向 T 成員的指標, 但是 T 不是類別型別
#include <iostream>



using namespace std;



template <typename T>

void func(int T::*) {

    cout << "is class" << endl;

}

template <typename>

void func(...) {

    cout << "not class" << endl;

}



struct Foo {};



int main(int argc, char *argv[]) {

    func<Foo>(0);       //輸出結果 : is class

    func<int>(0);       //輸出結果 : not class

}
  • 試圖在樣板引數中進行型別轉換
#include <iostream>



using namespace std;



template <int, int *>

void func() {

    cout << "int and int *" << endl;

}

template <int, int>

void func() {

    cout << "int and int" << endl;

}



int main(int argc, char *argv[]) {

    func<0, 0>();       //輸出結果 : int and int

    func<0, nullptr>();     //輸出結果 : int and int *

    func<nullptr, nullptr>();       //candidate template ignored: invalid explicitly-specified argument for 1st template parameter

}
  • 試圖將非法型別放入非型別樣板參數中
#include <iostream>



using namespace std;



template <typename T, T = T()>

struct S {};

template <typename T>

void f(S<T> * = nullptr) {

    cout << "S<T> *" << endl;

}

struct Foo {};



int main(int argc, char *argv[]) {

    f<int>();       //輸出結果 : S<T> *

    f<Foo>();       //candidate template ignored: substitution failure [with T = Foo]: a non-type template parameter cannot have type 'Foo'

}

與表達式相關的 SFINAE

  • 若函式參數列表或者樣板參數列表中含有病式的表達式

對於上述提到的都會導致替換失敗, 也就是函式在多載候選集合中被編碼器遺棄

接下來以 SFINAE 為例, 給出幾個典型的示例

1. add_lvalue_reference

C++ 標準樣板程式庫中就有這樣的類別, 我們以下稱為超函式. 這個超函式的作用是放入一個型別, 然後回傳這個型別的左值參考. 但是, 我們從前面可以看到, 當為 void 型別添加參考的時候, 也就是 void & 以及 void && 都是不合法的型別, 因此我們要為實作一個有這樣行為的超函式 : 當放入的型別可以被添加參考的時候, 那麼為其添加左值參考; 否則, 回傳原來的型別

有了以上思路, 我們首先要知道, 要想實作這樣一個超函式, 需要借助函式回傳型別以及 decltype 的幫助. 在《C++ 學習筆記》中, 我們曾經向大家介紹過 decltype, 大家曾經都有疑惑, 這個關鍵字看似沒什麼作用, 因為有 auto 更加方便. 但是 decltype 在樣板超編程中的作用十分強大, 樣板超編程中的很多函式是不需要被實作的, 它們只需要被宣告即可. 但是要想實際獲得這個函式的回傳型別, 需要呼叫它才行. 但是實際上它們沒有被實作, 所以並沒有辦法進行實際運作, 那麼 decltype 就允許我們在編碼器就獲得函式呼叫的結果的型別, 也就是回傳型別. 我們首先來看一個例子 :

template <typename T, typename U>

auto add(const T &t, const U &u) -> decltype(t + u);

一般情況下, 要想獲取上述函式的回傳型別, 一般採用以下方法 :
auto result {add(var_1, var_2)};
但是實際上, 在樣板超編程中, 我只需要用到 auto 推導得到的回傳型別, 而並不需要實際去呼叫運作這個 add 函式和 result 變數, 於是我們就用到了 decltype

我在之前的文章中介紹過, 當型別推導失敗, 編碼器就會放棄選擇這個函式. 也就是說, 如果給某個型別增加一個參考之後, 如果添加失敗 (你並不能給 void 型別添加一個參考), 那麼編碼器就會放棄選擇這個函式. 也就是說, 要想不產生編碼錯誤, 我們的函式多載集合必須至少有兩個函式, 並且在一個函式推導失敗之後, 另外一個函式無論如何都不可以產生任何編碼錯誤, 也就是它可以 "萬能匹配" 或著被 "任意匹配"

首先, 我們來實作一個函式來為任意給定的型別增加一個左值參考 :

template <typename T>

T &func(int);

這個函式不需要被實作, 或著說它根本不需要被呼叫, 呼叫它是不必要並且多餘的, 為了體現它不可以被呼叫, 我通常會在函式內部增加一個斷言結果永遠為 falsestatic_assert. 當然, 如果直接讓 static_assert 的第一個參數為 false, 那麼將永遠無法通過編碼 :

template <typename T>

T &func(int) {

    static_assert(false, "The function cannot be called!");     //error: static_assert failed "The function cannot be called!"

}

此時, 我們需要一個超函式的幫助, 讓這個推斷被延期到呼叫的時候才進行 :

template <typename T>

struct is_type {

    constexpr static auto value {true};

};

template <typename T>

T &func(int) {

    static_assert(!is_type<T>::value, "The function cannot be called!");     //OK

}

is_type 超函式是判定 T 是否為一個型別. 對於任意可以被放入的 T, 它一定是一個型別, 因此我們讓它的回傳值一直為 true, 然後以它作為 static_assert 的第一個參數, 這個時候, 由於 T 未知, 因此編碼器並不會立即對這個表達式進行計算, 它被延後到了第二個階段, 也就是函式呼叫的時候才進行計算 (這涉及到 C++ 的兩步名稱搜尋, 這個我們暫時不講). 但是上述函式雖然不會被呼叫, 但是也存在未定行為, 因為函式的回傳型別並不是 void, 但是函式並沒有 return 陳述式, 這在 Clang 中並不會引發編碼錯誤, 但是實際上在 GCC 或著 MSVC 中都會引發編碼錯誤, 因此我們為其增加一個 return 陳述式, 配合 C++ 11 的初始化列表, 讓編碼器自己來創建 :

template <typename T>

struct is_type {

    constexpr static auto value {false};

};

template <typename T>

T &func(int) {

    static_assert(is_type<T>::value, "The function cannot be called!");     //OK

    return {};

}

T 不是 void 時, 這個函式運作地不錯, 但是當 Tvoid 時就會引發編碼錯誤. 根據我們之前的需求, 我們要求如果某個型別無法被添加左值參考, 那麼應該回傳這個型別本身, 於是我們有 :

template <typename T>

struct is_type {

    constexpr static auto value {false};

};

template <typename T>

T &func(int) {

    static_assert(is_type<T>::value, "The function cannot be called!");     //OK

    return {};

}

template <typename T>

T func(...) {

    static_assert(is_type<T>::value, "The function cannot be called!");

    return {};

}

如果觀察得仔細, 那麼你可以發現, 參數列表從一開始就不為空, 這是因為 C++ 函式的多載是通過參數列表來區分的, 而不是回傳型別, 如果參數列表為空, 那麼無法統一函式的行為. 比如第一個函式參數列表為空, 那麼第二個函式的參數列表必須有參數才行, 而我們進行推導的時候, 只會執行一個表達式 :

func(); 或著 func(int_parameter);

否則的話, 函式的行為永遠都是確定的. 因此, 我們要增加一個參數, 這個參數通常為整型. 我們之前提到, 函式多載集合中至少要有一個函式, 如果某個需求用到的函式多載大於 2 個, 那麼我們可以選擇 longlong long 甚至含有 unsigned 的型別作為參數, 在這樣的需求上, 沒有比整形更合適的型別了

現在, 我們可以通過

decltype(func<T>(0));

來推導回傳型別了, 接著我們將這個行為實作為類別

#include <iostream>



using namespace std;



template <typename T>

struct is_type {

    constexpr static auto value {false};

};

template <typename T>

T &func(int) {

    static_assert(is_type<T>::value, "The function cannot be called!");     //OK

    return {};

}

template <typename T>

T func(...) {

    static_assert(is_type<T>::value, "The function cannot be called!");

    return {};

}

template <typename T>

struct _add_lvalue_reference {

    using type = decltype(func<T>(0));

};



int main(int argc, char *argv[]) {

    cout << boolalpha;

    cout << is_same<_add_lvalue_reference<int>::type, int &>::value << endl;        //輸出結果 : true

    cout << is_same<_add_lvalue_reference<int *>::type, int *&>::value << endl;     //輸出結果 : true

    cout << is_same<_add_lvalue_reference<int &>::type, int &>::value << endl;      //輸出結果 : true

    cout << is_same<_add_lvalue_reference<void>::type, void>::value << endl;        //輸出結果 : true

}

2. is_assignable

這個超函式的作用是測試某一個型別的變數是否可以指派給另外一個型別, 比如 int a = 'a'; 是合法的, 它會發生隱含型別轉換; 但是 int a = (int *)0x0; 是不合法的. 因此我們希望有一個超函式可以測試某個指派是否合法

這個超函式的實作方式和上面的有一些不太一樣, 我們不但需要借助 decltype 的幫助, 還需要另外一個樣板超編程的重要成員 declval 的幫助. declval 是一個函式, 它也是不建議被明確呼叫的, 它產生某個型別對應的右值物件, 不管某個型別是否有建構子或者它是否可以被建構. 我直接展示出 declval 的實作方式 :

template <typename T>

T &&declval_impl(int);

template <typename T>

T declval_impl(long);

template <typename T>

decltype(declval_impl<T>(0)) declval();

與表達式相關的 SFINAE小節中, 我們提到了 SFINAE 也可以用於表達式. 於是, 結合之前我們所學習的, 我們可以推斷, 我們的需求是如果某個指派合法, 那麼我們希望回傳一個表示合法的結果; 否則, 需要回傳一個表示不合法的結果. 在樣板超編程中, 我們像 true 或著 false 那樣的明確表達真與假的表達式, 因此我們可以創建一個專門的型別來表示真與假 :

struct true_type {};

struct false_type {};

一般來說, 這兩個型別不可以被創建任何物件, 因此它們的建構子和解構子都是被刪除的函式, 此處方便起見, 我就不再明確寫出來了

借助 declval, 我們可以寫出需要測試的表達式 : declval<T>() = declval<U>(), 如果能夠寫出這個, 那麼接下來的實作就並不困難了. 要像實作這樣一個函式, 還有一個問題, 這個表達式我們應該放置在何處? 首先我們想到參數列表, 但是參數列表會顯得過於繁雜, 並且表達式並不能直接放在參數列表中, 如果轉換為型別還要考慮預設引數的問題. 因此, 我們放在回傳型別位置. 此處, 我有一個小技巧, 這個小技巧大量存在於 C++ 標準樣板程式庫中 :

template <typename, typename T>

struct select_second_type {

    using type = T;

};

我們把 declval<T>() = declval<U>() 放置於 decltype 中, 然後將推導的型別作為 select_second_type 的第一個樣板引數, 因為這個引數用不到, 所以它並不具名. 於是, 我們可以開始實作 is_assignable :

#include <iostream>



template <typename T>

T &&declval_impl(int);

template <typename T>

T declval_impl(long);

template <typename T>

decltype(declval_impl<T>(0)) declval();

struct true_type {};

struct false_type {};

template <typename, typename T>

struct select_second_type {

    using type = T;

};



template <typename T, typename U>

typename select_second_type<decltype(declval<T>() = declval<U>()), true_type>::type test_is_assignable(int);

template <typename, typename>

false_type test_is_assignable(...);



template <typename, typename>

struct is_same {

    constexpr static auto value {false};

};

template <typename T>

struct is_same<T, T> {

    constexpr static auto value {true};

};



template <typename T, typename U>

struct is_assignable {

    constexpr static auto value {is_same<decltype(test_is_assignable<T, U>(0)), true_type>::value};

};



int main(int argc, char *argv[]) {

    std::cout << std::boolalpha;

    std::cout << is_assignable<int &, int>::value << std::endl;     //輸出結果 : true

    std::cout << is_assignable<int &, char>::value << std::endl;        //輸出結果 : true

    std::cout << is_assignable<int &, void>::value << std::endl;        //輸出結果 : false

    std::cout << is_assignable<int &, int *>::value << std::endl;       //輸出結果 : false

    std::cout << is_assignable<int &, int &>::value << std::endl;       //輸出結果 : true

    std::cout << is_assignable<int &, int &&>::value << std::endl;      //輸出結果 : true

    std::cout << is_assignable<int &, double>::value << std::endl;      //輸出結果 : true

    std::cout << is_assignable<int, int>::value << std::endl;       //輸出結果 : false

    std::cout << is_assignable<int &&, int>::value << std::endl;        //輸出結果 : false

    std::cout << is_assignable<int, int &&>::value << std::endl;        //輸出結果 : false

    std::cout << is_assignable<int, int &>::value << std::endl;     //輸出結果 : false

}

此處要解釋一下為什麼類似於 is_assignable<int, int>::valueis_assignable<int &&, int>::value 會回傳 false. 我們都已經知道, 像是 5 = 0 這種表達式是不合法的, 因為不可以對右值進行指派, 而 int 或著 int && 正是這樣的. 因此, 它們的物件無法被指派, 也就導致了結果是 false