在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為程序員提供了那些模板在編譯期間被摒棄掉,那些在特定的模板參數類型下要使用。從而選擇正確的模板進行編譯匹配。