# 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。 雖然看起來非常複雜但仔細理解後又是一個很漂亮的寫法,大家不訪可以試試看!