C++ enable_if 探究


在C++11中充斥着大量的enable_if, 那么enable_if是什么呢,為何要引入它呢,它有什么作用呢,,,,

在這之前,我們先了解下C++模板推導的機理。

1. 前序:C++模板推導機理

模板推導過程中,編譯器會根據具體調用時的類型,來進行模板推導,並找出最適合的一個模板,注意是最適合的,所以編譯器需要把所有對應的模板全部看一遍,才能知道哪個是最適合的。

比如:

//1
void func(int a, int b){
   cout << "int and int"<<endl;
}
//2
template<typename T1, typename T2>
void func(T1 a, T2 b){
	cout << "T1 and T2" <<endl;
}
//3
template<typename T>
void func(T a, T b){
	cout <<"T and T" <<endl;
}
//4
template<>
void func<bool, int>(bool a, int b){
	cout << "bool and int <>" <<endl;
}
//5
template<>
void func<int, int>(int a, int b){
	cout << "int and int <>" <<endl;
}
//6
template<bool,  typename T>
void func(bool a, T b){
	cout << "bool and T" <<endl;
}

int main(){
    func(5, 6);                     // int and int
    func(5U, 6);                    // T1 and T2
    func(5U, 6U);                   //T and T
    func(false, 6U);                // T1 and T2
    func(false, 6);                 // bool and int <>
    func<false, int>(false, 6U);    // bool and T
    return 0;
}

C++在函數模板推導之前,會根據形參類型,首先查看有沒有形參匹配的非模板函數void func(int a, int b),如果匹配,則直接會調用此函數。如果不匹配,則會開始查找模板。首先會找到個最泛化的模板版本template<typename T1, typename T2> void func(T1 a, T2 b),再找特化的版本template<> void func<bool, int>(bool a, int b) , 對於template<typename T> void func(T a, T b)可以理解為是泛化版本中滿足連個形參相同的一個函數定義。template<bool, typename T> void func(bool a, T b),只是利用了函數模板傳參而已(template<bool a, typename T>, 我省列了a)。

  • func(5,6)會直接找非模板版本,此時找到了void func(int a, int b),那么就不會接着往下找模板版本了(包括模板的特化版本,這也是為何輸出是int and int而非 int and int <>的原因)。

  • func(5U,6)第一個形參unsigned int與void func(int a, int b)不匹配 ,此時便會尋找模板,在模板中,發現template<typename T1, typename T2> void func(T1 a, T2 b)匹配。而其他的模板都不匹配。

  • func(5U, 6U),同理,會發現,模板中兩個匹配分別為template<typename T1, typename T2> void func(T1 a, T2 b)template<typename T> void func(T a, T b),但顯然后一個要求更苛刻(兩個形參類型相同)所以這里就選擇了后一個template<typename T> void func(T a, T b)。(注意,此處並非偏特化,c++函數不支持偏特化,類模板才支持)。

  • func(false, 6U),同理,template<typename T1, typename T2> void func(T1 a, T2 b)滿足。template<> void func<bool, int>(bool a, int b)特化為了bool和int,第二個參數不是unsigned int, 所以不滿足。

  • func(false, 6),同理,template<typename T1, typename T2> void func(T1 a, T2 b)滿足,template<> void func<bool, int>(bool a, int b)也滿足,但是后者是特化版本,所以調用了template<> void func<bool, int>(bool a, int b)

  • func<false, int>(false, 6U),此處顯示聲明了帶參模板類型為<false, unsigned int>, 由於帶參的模板函數只有template<bool, typename T> void func(bool a, T b), 此時會調用此函數,並且6U會隱式轉化為int類型。

現在函數模板的推導調用順序已經清楚了。那么出現了一個新問題:

在C++模板類中由於有泛型模板、特化、偏特化,那么就會出現很多的模板定義,當這些模板類作為形參傳遞給模板函數時,由於函數模板推導需要把所有的模板進行參數替換匹配一遍,那么就可能會出現一個問題,也就是有些模板函數會出現錯誤,如果編譯器進行報錯的話,那么就會使模板的編程變得特別復雜,那么如何才能解決這個問題呢。由此引入了SFINAE機制(Substitution Failure Is Not An Error)。

2. SFINAE

且看這個例子:

int add(int a,  int b){
    return a+b;
}
template<typename T>
typename T::value_type add(const T& a, const T& b){
    return a+b;
}
int main(){
    add(6U, 5U); //result:11, called the first function, because of the second would fail.
}

add(6U, 5U)的形參是unsigned int不滿足int add(int a, int b),會進行調用模板的版本,進行模板推導,但是顯然unsigned int::value_type 是錯誤的,unsigned int 內沒有value_type,但是編譯器沒有報錯,並且輸出了結果為11。這是因為在推導模板過程發現了錯誤,摒棄了模版template<typename T> typename T::value_type add(const T& a, const T& b),轉而調用int add(int a, int b)並對形參進行了隱式轉換(unsigned int --> int)。

在最新的C++11標准中,闡述了當某些模板替換失敗時,比如下面的例子,那么不進行報錯,編譯器會忽略這個在替換過程中出現錯誤的模板版本,轉而看其他的可選模板。此標准的陳述如下:

If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed if written using the substituted arguments. Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.

如果所有的模板都出錯被丟棄而導致都沒有匹配上怎么辦,答案就是會編譯報錯

3. enable_if

有了上述的鋪墊,我們現在再看看enable_if。

3.1 什么是enable_if.

源碼中的定義如下:

// Primary template.
/// Define a member typedef @c type only if a boolean constant is true.
template<bool, typename _Tp = void>
    struct enable_if
    { };

// Partial specialization for true.
template<typename _Tp>
    struct enable_if<true, _Tp>
    { typedef _Tp type; };

其實 enable_if 是可用用來幫助編譯器在編譯期間進行模板選擇的struct。利用了c++的traits編程技法。

這兩個版本的定義中,第一個是一個一般的泛化模板定義,第二個是偏特化定義。當模板的第一個參數是true時,就會調用偏特化的版本,而不是第一個泛化的版本。

enable_if<true, T> //用的是偏特化的版本
enable_if<false, T> // 用的是泛化的

3.2 enable_if的使用方法

下面為enable_if的一種使用方法:

//定義一個person類
class person{};

//定義一個dog類
class dog{};

//用來判斷是否是person類
template<typename T>
struct is_person{
   static const bool value = false;
} ;

template<>
struct is_person<person>{
   static const bool value = true;
};

//當T為person時,被調用
template<typename T, typename std::enable_if<is_person<T>::value, T> :: type* = nullptr>
void func(T t){
    // do something;
    cout << "person stuff"<<endl;
}

void func(int a){
    cout << "int type" <<endl;
}

//當T不是person時,被調用
template<typename T, typename std::enable_if<!is_person<T>::value, T> :: type* = nullptr>
void func(T t){
    // do something;
    cout << "not person stuff"<<endl;
}

int main(){
    func(5);  		// int type
    func(person());     // person stuff
    func(dog());	// not person stuff
}

這個例子中,我們定義了is_person結構體,其person的特化版本內的value = true,泛化版本value = false,由此結合enable_if的定義,以及前面介紹的SFINAE,我們不難得出上面的輸出結果。因為enable_if<true, T>::type是沒有錯誤的,但enable_if<false, T>內並無type,所以會出錯丟棄對應的出錯模板,采用其他模板。

enable_if可能看起來使用比較麻煩,C++14引入了一個enable_if_t ,其定義如下:

template <bool B, typename T = void>
using enable_if_t = typename enable_if<B, T>::type;

所以可以用enable_if_t來替換上面的enable_if。其實感覺並沒有書寫簡化多少。

4. 總結

C++基於自身的SFIANE法則,利用函數模板在推導過程中把出錯的模板摒棄掉,進而選擇最匹配的模板板門進行調用。enable_if為程序員提供了那些模板在編譯期間被摒棄掉,那些在特定的模板參數類型下要使用。從而選擇正確的模板進行編譯匹配。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM