# C++ `std::enable_if` 和 SFINAE
[toc]
## 使用時機
```cpp
#include <iostream>
#include <string>
#include <vector>
#include <set>
template< typename T >
std::string ToString( const T& x )
{
return std::to_string( x );
}
int main( int argc, char** argv )
{
int a = 35;
float b = 3.14f;
double c = 25.12;
char d = 'C';
std::cout << ToString( a ) << std::endl;
std::cout << ToString( b ) << std::endl;
std::cout << ToString( c ) << std::endl;
std::cout << ToString( d ) << std::endl;
return 0;
}
```
這種 template function 如果當你要給 `std::vector` 的時候你就會遇到編譯錯誤:
```cpp
std::vector< int > k1;
k1.push_back( 5 );
k1.push_back( 8 );
k1.push_back( 3 );
std::cout << ToString( k1 ) << std::endl; // compile error: C2665
```
當然很明顯的就是因為 `std::vector` 不能直接丟到 `std::to_string()` 裡面,需要有特殊處理,為此我們可以對該 `ToString()` template 特化一個版本:
```cpp=
template< typename T >
std::string ToString( const std::vector< T >& vector )
{
std::string r;
for ( const auto& temp : vector )
{
r += ToString( temp ) + " ";
}
return r;
}
```
可是這時候就會發現如果換 `std::set` 的時候又會 compile failed,變成說又要寫出一個 `std::set` 版本的出來:
```cpp
std::set< char > k2;
k2.insert( 'J' );
k2.insert( 'C' );
k2.insert( 'e' );
std::cout << ToString( k2 ) << std::endl; // compile error: C2665
```
```cpp
template< typename T >
std::string ToString( const std::set< T >& set )
{
std::string r;
for ( const auto& temp : set )
{
r += ToString( temp ) + " ";
}
return r;
}
```
但 C++ 容器還有所謂的 `std::map`、`std::unordered_map`、`std::array` 還有 `std::list` 等,如果每個 container 都要寫一個特化版本且邏輯都一樣的情況下是很擾人的事情。
這個時候就很適合使用 `std::enable_if`。
## `std::enable_if`
先介紹一下什麼是 `std::enable_if`,我們先來看一下它的實作:
```cpp
template < bool B, typename T = void >
struct enable_if {}; // primary one
template < typename T >
struct enable_if< true, T > { typedef T type; }; // specialized one
```
> If B is `true`, std::enable_if has a public member typedef type, equal to T; otherwise, there is no member typedef;
非常有趣的一個東西,我知道這第一次接觸的時候會看不懂這是什麼東西,基本上你可以這樣看:第一個 `enable_if` 這個 template 參數有兩個,第一個是接收 non-type parameter 也就是 bool,另外一個是接收型別,然後它就是產生一個空的 `struct` 叫 `enable_if`。
第二個 `enable_if` 是第一個版本的特化版,它主要是處理說當第一個 non-type parameter 是 `true` 的時候,一樣會產生一個 `struct` 叫 `enable_if`,但這次不一樣的是會多一個 member `type`,然後它是 `typedef T`,也就是說 `type` 是型別 `T` 的別名。
講這麼多,我們還是來用看看吧,首先 `std::enable_if` 是從 c++ 11 舊有的,你只需要 include `<type_traits>` 就可以用了。
```cpp
std::enable_if< true >::type* a // 型態是 void*
std::enable_if< true, int >::type b; // 型態是 int
std::enable_if< false > c // 型態是 std::enable_if
std::enable_if< false, int >::type d; // 錯誤該 member 不存在
```
我們可以看到說,只要你第一個參數是 `false` 的情況下,它就不存在 `type` 這個成員,只有當你第一個參數是 `true` 的時候才有;如果沒有指定型別的情況下,預設值是 `void`。
那到底這東西有什麼實質用途呢,主要它是要搭配 C++ 的一個機制 "SFINAE"。
## SFINAE
SFINAE 的全稱叫「替換失敗不是一個錯誤」(Substitution Failure Is Not An Error),是指說 C++ 在處理重載函數(overload)機制時,它的邏輯是會產生一個可能的重載候選集,當你有使用到的時候會先把 template 都嘗試實例化,但這個過程中如果出現替換失敗,編譯器並不會當成是編譯錯誤而停止編譯,而只會把該 template 從候選集合中移除掉而已。
譬如這樣:
```cpp=
struct Test
{
typedef int foo;
};
template < typename T >
void f( typename T::foo ) {} // Definition #1
template < typename T >
void f( typename T ) {} // Definition #2
int main( int argc, char** argv )
{
f< Test >( 10 ); // call #1
f< int >( 10 ); // call #2
return 0;
}
```
注意看當我們呼叫 `f< int >` 時,編譯器會嘗試推導 `f( typename T::foo )` 和 `void f( typename T )`,把 `T` 都替換成 `int`。然後發現了 `f( typename T::foo )` 這個版本會替換失敗,因為 `int` 並沒有 `type` 這個成員;這就是所謂的替換失敗,但編譯不會失敗,只是把這個版本的 function 從候選集合中刪除掉。如此一來就只剩下 `void f( typename T )` 這個版本。
> 如果你把 Definition #2 版刪除的話就會發生 compile error C2672: no matching overload function found.
解釋完了 SFINAE 和 `std::enable_if`,我們來實際應用在剛剛的範例上吧。
## 實際用途
基本上就是我們可以利用 `std::enable_if` 來當一個開關,然後利用 C++ 的 SFINAE 的機制來控制當某個條件符合時就用哪個 function 這樣。譬如剛剛的範例來說,我們可以先自己寫一個 template 來判斷說傳入的類型是否是 container:
```cpp=
template < typename T >
struct is_type_container
{
static const bool value = false; // compile time constant
}; // primary
template < typename T >
struct is_type_container< std::vector< T > >
{
static const bool value = true;
}; // specialized #1
template < typename T >
struct is_type_container< std::set< T > >
{
static const bool value = true;
}; // specialized #2
```
我們設計了一個 template 當你傳入一個 `T` 時,會有一個 `bool` 成員 `value` 是 `false`。而我們特化兩個版本分別是 `std::vector` 和 `std::set`,然後 `value` 都會是 `true`。
接著我們改造一下原本我們的 `ToString()`:
```cpp=
template< typename T >
std::string ToString( const T& x, typename std::enable_if< !is_type_container< T >::value, T >::type* = 0)
{
return std::to_string( x );
}
template< typename T >
std::string ToString( const T& x, typename std::enable_if< is_type_container< T >::value, T >::type* = 0 )
{
std::string r;
for ( const auto& temp : x )
{
r += ToString( temp ) + " ";
}
return r;
}
```
看起來好像非常複雜,但其實我們一個一個來推敲就可以知道其實很簡單的,我們來想像一下,當你傳入的型別 `T` 是 `std::vector< int >` 時,它的替換邏輯大概是這樣:`is_type_container< T >::value` 會是 `true`,接著 `std::enable_if< true, T >::type` 由於先前我們說過,第一個參數是 `true` 所以該成員 `type` 是存在的,所以這個 function 成立。
那它最後解析個結果,會是 `T*` 型態,也就是 `std::vector< int >*`,預設值是 0 也就是 `nullptr`。注意 C++ 是可以接受你沒使用的參數是 unnamed 的,而確實在這個情況下來說我們是根本用不到這個參數的,我們只是用來控制它是否替換成功罷了。
> 其實 `enable_if` 的第二個參數沒有很重要,有人會寫 `T`,有人會寫 `void` 或是 `void*`,甚至你不寫也可以(預設就是 void)。
那我們可以反看另一個 `ToString()`,它是非 container 版本的,所以 `std::enable_if< !is_type_container< T >::value, T >::type` 這個對於 `std::vector< int >` 來說,因為他就是 container 所以成員 `type` 不成立,由於 C++ 的 SFINAE 機制,這個版本的 function 就會從多載候選集合中刪除掉。
> 如果你沒有創建 `std::string ToString( const T& x, typename std::enable_if< !is_type_container< T >::value, T >::type* = 0)` 這個版本,使用的是原本的 `std::string ToString( const T& x )` 的話,你傳入 `std::vector` 或是 `std::set` 都會造成模擬兩可著狀態(因為有兩個以上可使用的 function),造成編譯 C2668 錯誤: ambiguous call to overload function。
雖然看起來非常複雜但仔細理解後又是一個很漂亮的寫法,大家不訪可以試試看!