## enable_if
### 頭文件: `"boost/utility/enable_if.hpp"`
有時候,我們希望控制某個函數或類模板的特化是否可以加入到重載決議時使用的重載或特化的集合中。例如,考慮一個重載的函數,它有一個版本是帶一個`int`參數的普通函數,另一個版本是一個函數模板,它要求參數類型 `T` 具有一個名為`type`的嵌套類型。它們看起來可能象這樣:
```
void some_func(int i) {
std::cout << "void some_func(" << i << ")\n";
}
template <typename T> void some_func(T t) {
typename T::type variable_of_nested_type;
std::cout <<
"template <typename T> void some_func(" << t << ")\n";
}
```
現在,想象一下當你在代碼中調用 `some_func` 將發生什么。如果參數的類型為 `int`, 第一個版本將被調用。如果參數的類型是 `int`以外的其它類型,則第二個(模板)版本將被調用。
這沒問題,只要這個類型有一個名為`type`的嵌套類型,但如果它沒有,這段代碼就不能通過編譯。這會是一個問題嗎?好的,考慮一下如果你用其它整數類型來調用,如`short`, 或 `char`, 或 `unsigned long`,那么又會發生什么。
```
#include <iostream>
void some_func(int i) {
std::cout << "void some_func(" << i << ")\n";
}
template <typename T> void some_func(T t) {
typename T::type variable_of_nested_type;
std::cout <<
"template <typename T> void some_func(" << t << ")\n";
}
int main() {
int i=12;
short s=12;
some_func(i);
some_func(s);
}
```
編譯這段程序時,你將從失敗的編譯器中得到類似以下的輸出:
```
enable_if_sample1.cpp: In function 'void some_func(T)
[with T = short int]':
enable_if_sample1.cpp:17: instantiated from here
enable_if_sample1.cpp:8: error:
'short int' is not a class, struct, or union type
Compilation exited abnormally with code 1 at Sat Mar 06 14:30:08
```
就是這樣。`some_func` 的模板版本被選為最佳的重載,但這個版本中的代碼對于類型`short`而言是無效的。我們怎樣才能避免它呢?好的,我們希望僅對含有名為type的嵌套類型的類使用模板版本的 `some_func` ,而對于其它沒有這個嵌套類型的類則忽略它。我們能夠做到。最簡單的方法,但不一定是實際中總能使用的方法,是把模板版本的返回類型改為如下:
```
template <typename T> typename T::type* some_func(T t) {
typename T::type variable_of_nested_type;
std::cout <<
"template <typename T> void some_func(" << t << ")\n";
return 0;
}
```
如果你沒有學過 SFINAE (匹配失敗不是錯誤),\[8\] 很可能現在你的臉上會有困惑的表情。編譯修改過的代碼,我們的例子會通過編譯。`short` 被提升為 `int`, 并且第一個版本被調用。這種令人驚奇的行為的原因是模板版本的 `some_func` 不再包含在重載決議的集合內了。它被排除在內是因為,編譯器看到了這個函數的返回類型要求模板類型`T` 要有一個嵌套類型`type` ,而它知道 `short` 不滿足這個要求,所以它把這個函數模板從重載決議集合中刪掉了。這就是 Daveed Vandevorde 和 Nicolai Josuttis 教給我們的 SFINAE, 它意味著寧可對有問題的類型不考慮函數的重載,也不要產生一個編譯器錯誤。如果類型有一個符合條件的嵌套類型,那么它就是重載決議集合的一部分。
> \[8\] 見參考書目[3]。
```
class some_class {
public:
typedef int type;
};
int main() {
int i=12;
short s=12;
some_func(i);
some_func(s);
some_func(some_class());
}
```
運行該程序的輸出如下:
```
void some_func(12)
void some_func(12)
template <typename T> void some_func(T t)
```
這種辦法可以用,但它不太好看。在這種情形下,我們可以不管原來的 `void` 返回類型,我們可以用其它類型替換它。但如果不是這種情形,我們就要給函數增加一個參數并給它指定一個缺省值。
```
template <typename T>
void some_func(T t,typename T::type* p=0) {
typename T::type variable_of_nested_type;
std::cout << "template <typename T> void some_func(T t)\n";
}
```
這個版本也是使用 SFINAE 來讓自己不會被無效類型所使用。這兩種解決方案的問題都在于它們有點難看,我們把它們弄成了公開接口的一部分,并且它們只能在某些情形下使用。Boost 提供了一個更干凈的解決方法,這種方法不僅在語法上更好看,而且提供了比前面的解決方法更多的功能。
### 用法
要使用 `enable_if` 和 `disable_if`, 就要包含頭文件 `"boost/utility/enable_if.hpp"`. 在第一個例子中,我們將禁止第二個版本的 `some_func` ,如果參數的類型是整型的話。象一個類型是否整型這樣的類型信息可以用另一個Boost庫`Boost.Type_traits`來取得。`enable_if` 和 `disable_if` 模板都通過接受一個謂詞來控制是否啟用或禁止一個函數。
```
#include <iostream>
#include "boost/utility/enable_if.hpp"
#include "boost/type_traits.hpp"
void some_func(int i) {
std::cout << "void some_func(" << i << ")\n";
}
template <typename T> void some_func(
T t,typename boost::disable_if<
boost::is_integral<T> >::type* p=0) {
typename T::type variable_of_nested_type;
std::cout << "template <typename T> void some_func(T t)\n";
}
```
雖然這看起來與我們前面所做的差不多,但它表達了一些我們使用直接的方法所不能表達的東西,而且它在函數的聲明中表達了關于這個函數的重要信息。看到這些,我們可以清楚的知道這個函數要求類型`T`不能是一個整數類型。如果我們希望僅對含有嵌套類型`type`的類型啟用這個函數,它也可以做得更好,而且我們還可以用另一個庫Boost.Mpl\[9\] 來做。如下:
> \[9\] Boost.Mpl 超出了本書的范圍。訪問 [http://www.boost.org](http://www.boost.org) 獲得更多關于Mpl的信息。另外,也可以看一下David Abrahams 和 Aleksey Gurtovoy 的書, C++ Template Metaprogramming!
```
#include <iostream>
#include "boost/utility/enable_if.hpp"
#include "boost/type_traits.hpp"
#include "boost/mpl/has_xxx.hpp"
BOOST_MPL_HAS_XXX_TRAIT_DEF(type)
void some_func(int i) {
std::cout << "void some_func(" << i << ")\n";
}
template <typename T> void some_func(T t,
typename boost::enable_if<has_type<T> >::type* p=0) {
typename T::type variable_of_nested_type;
std::cout << "template <typename T> void some_func(T t)\n";
}
```
這真的很酷!我們現在可以對沒有嵌套類型`type`的`T`禁用`some_func`的模板版本了,而且我們清晰地表達了這個函數的要求。這里的竅門在于使用了Boost.Mpl的一個非常漂亮的特性,它可以測試任意類型`T`是否內嵌有某個指定類型。通過使用宏 `BOOST_MPL_HAS_XXX_TRAIT_DEF(type)`, 我們定義了一個名為`has_type`的新的trait,我們可以在函數`some_func`中使用它作為`enable_if`的謂詞。如果謂詞為`True`, 這個函數就是重載決議集合中的一員;如果謂詞為 `false`, 這個函數就將被排除。
也可以包裝返回類型,而不用增加一個額外的(缺省)參數。我們最后一個也是最好的一個 `some_func`, 在它的返回類型中使用 `enable_if` ,如下:
```
template <typename T> typename
boost::enable_if<has_type<T>,void>::type
some_func(T t) {
typename T::type variable_of_nested_type;
std::cout << "template <typename T> void some_func(T t)\n";
}
```
如果你需要返回你想啟用或禁用的類型,那么在返回類型中使用 `enable_if` 和 `disable_if` 會比增加一個缺省參數更合適。另外,有可能有的人真的為缺省參數指定一個值,那樣就會破壞這段代碼。有時,類模板的特化也需要被允許或被禁止,這時也可以使用 `enable_if`/`disable_if` 。不同的是,對于類模板,我們需要對主模板進行一些特別的處理:增加一個模板參數。考慮一個帶有返回一個`int`的成員函數`max`的類模板:
```
template <typename T> class some_class {
public:
int max() const {
std::cout << "some_class::max() for the primary template\n";
return std::numeric_limits<int>::max();
}
};
```
假設我們決定對于所有算術類型(整數類型及浮點數類型), 給出一個特化版本的定義,`max` 返回的是該算術類型可以表示的最大值。那么我們需要對模板類型`T`使用`std::numeric_limits`,而對其它類型我們還是使用主模板。要做到這樣,我們必須給主模板加一個模板參數,該參數的缺省類型為 `void` (這意味著用戶不需要顯式地給出該參數)。結果主模板的定義如下:
```
template <typename T,typename Enable=void> class some_class {
public:
int max() const {
std::cout << "some_class::max() for the primary template\n";
return std::numeric_limits<int>::max();
}
};
```
現在我們已經為提供特化版本作好了準備,該特化版本為算術類型所啟用。該特性可通過 Boost.Type_traits 庫獲得。以下是特化版本:
```
template <typename T> class some_class<T,
typename boost::enable_if<boost::is_arithmetic<T> >::type> {
public:
T max() const {
std::cout << "some_class::max() with an arithmetic type\n";
return std::numeric_limits<T>::max();
}
};
```
該版本只有當實例化所用的類型為算術類型時才會啟用,這時特性 `is_arithmetic` 為 `true`. 它可以正常工作是因為 `boost::enable_if<false>::type` 是 `void`, 會匹配到主模板。以下程序用不同的類型測試這個模板:
```
#include <iostream>
#include <string>
#include <limits>
#include "boost/utility/enable_if.hpp"
#include "boost/type_traits.hpp"
// Definition of the template some_class omitted
int main() {
std::cout << "Max for std::string: " <<
some_class<std::string>().max() << '\n';
std::cout << "Max for void: " <<
some_class<void>().max() << '\n';
std::cout << "Max for short: " <<
some_class<short>().max() << '\n';
std::cout << "Max for int: " <<
some_class<int>().max() << '\n';
std::cout << "Max for long: " <<
some_class<long>().max() << '\n';
std::cout << "Max for double: " <<
some_class<double>().max() << '\n';
}
```
我們預期前兩個 `some_class` 會實例化主模板,剩下的將會實例化算術類型的特化版本。運行該程序可以看到的確如此。
```
some_class::max() for the primary template
Max for std::string: 2147483647
some_class::max() for the primary template
Max for void: 2147483647
some_class::max() with an arithmetic type
Max for short: 32767
some_class::max() with an arithmetic type
Max for int: 2147483647
some_class::max() with an arithmetic type
Max for long: 2147483647
some_class::max() with an arithmetic type
Max for double: 1.79769e+308
```
一切正常!以前,要允許或禁止重載函數和模板特化需要一些編程的技巧,多數看到代碼的人都不能完全明白。通過使用 `enable_if` 和 `disable_if`, 代碼變得更容易寫也更容易讀了,并且可以從聲明中自動獲得正確的類型要求。在前面的例子中,我們使用了模板 `enable_if`, 它要求其中的條件要有一個名為`value`的嵌套定義。對于多數可用于元編程的類型而言這都是成立的,但對于整型常量表達式則不然。如果沒有名為`value`的嵌套類型,就要使用 `enable_if_c` 來代替,它接受一個整型常量表達式。使用 `is_arithmetic` 并直接取出它的值,我們可以這樣重寫`some_class`的啟用條件:
```
template <typename T> class some_class<T,
typename boost::enable_if_c<
boost::is_arithmetic<T>::value>::type> {
public:
T max() const {
std::cout << "some_class::max() with an arithmetic type\n";
return std::numeric_limits<T>::max();
}
};
```
`enable_if` 和 `enable_if_c`原則上并沒有不同。它們的區別僅在于是否要求有嵌套類型value。
### 總結
被稱為SFINAE的C++語言特性是很重要的。沒有它,很多新的代碼會破壞已有的代碼,并且某些類型的函數重載(以及模板特化)將會無法實現。直接使用SFINAE來控制特定的函數或類型,使之被允許或被禁止用于重載決議,會很復雜。這樣也會產生難以閱讀的代碼。使用 `boost::enable_if` 是更好的辦法,它可以規定重載僅對某些特定類型有效。如果相同的參數用于 `disable_if`, 則規定重載對于符合條件的類型無效。雖然使用SFINAE也可以實現,但該庫可以更好地表達相關意圖。本章忽略了`enable_if` 和 `disable_if`的lazy版本(名為 `lazy_enable_if` 和 `lazy_disable_if`), 不過我在這里簡單地提及一下。lazy版本被用于避免實例化類型可能無效的情形(取決于條件的取值).
以下情形時使用 `enable_if` :
* 你需要在把一個符合某些條件的函數加入到或排除出重載決議集合中。
* 你需要根據某個條件將一個類模板的特化版本加入到或排除出特化集合中。
- 序
- 前言
- Acknowledgments
- 關于作者
- 本書的組織結構
- Boost的介紹
- 字符串及文本處理
- 數 據結構, 容器, 迭代器, 和算法
- 函數對象及高級編程
- 泛 型編程與模板元編程
- 數學及數字處理
- 輸入/輸出
- 雜項
- Part I: 通用庫
- Library 1. Smart_ptr
- Smart_ptr庫如何改進你的程序?
- 何時我們需要智能指針?
- Smart_ptr如何適應標準庫?
- scoped_ptr
- scoped_array
- shared_ptr
- shared_array
- intrusive_ptr
- weak_ptr
- Smart_ptr總結
- Library 2. Conversion
- Conversion 庫如何改進你的程序?
- polymorphic_cast
- polymorphic_downcast
- numeric_cast
- lexical_cast
- Conversion 總結
- Library 3. Utility
- Utility 庫如何改進你的程序?
- BOOST_STATIC_ASSERT
- checked_delete
- noncopyable
- addressof
- enable_if
- Utility 總結
- Library 4. Operators
- Operators庫如何改進你的程序?
- Operators
- 用法
- Operators 總結
- Library 5. Regex
- Regex庫如何改進你的程序?
- Regex 如何適用于標準庫?
- Regex
- 用法
- Regex 總結
- Part II: 容器及數據結構
- Library 6. Any
- Any 庫如何改進你的程序?
- Any 如何適用于標準庫?
- Any
- 用法
- Any 總結
- Library 7. Variant
- Variant 庫如何改進你的程序?
- Variant 如何適用于標準庫?
- Variant
- 用法
- Variant 總結
- Library 8. Tuple
- Tuple 庫如何改進你的程序?
- Tuple 庫如何適用于標準庫?
- Tuple
- 用法
- Tuple 總結
- Part III: 函數對象與高級編程
- Library 9. Bind
- Bind 庫如何改進你的程序?
- Bind 如何適用于標準庫?
- Bind
- 用法
- Bind 總結
- Library 10. Lambda
- Lambda 庫如何改進你的程序?
- Lambda 如何適用于標準庫?
- Lambda
- 用法
- Lambda 總結
- Library 11. Function
- Function 庫如何改進你的程序?
- Function 如何適用于標準庫?
- Function
- 用 法
- Function 總結
- Library 12. Signals
- Signals 庫如何改進你的程序?
- Signals 如何適用于標準庫?
- Signals
- 用法
- Signals 總結