C++“準”標準庫Boost學習指南(3):Boost.Utility
一些本不應在一個庫裡出現的有用的東西,只是因為它們每個都不太複雜和廣泛,不足夠形成一個單獨的庫。但不是說它們沒有什麼用外;事實上小的工具通常都有最廣泛的用處。在Boost, 這些小工具被集中起來,形成一個稱為Utility的庫。你可以在這找到checked_delete, 一個函式,用於確認在刪除點的型別是完整的;還有類noncopyable,用於確保類不能被複制;還有enable_if,用於對函式過載的完全控制。
Utility 庫如何改進你的程式?
- 編譯期斷言 BOOST_STATIC_ASSERT
- 安全的析構 checked_delete 和 checked_array_delete
- 禁止複製 noncopyable
- operator&被過載時用 addressof取得物件地址
- 用enable_if 和 disable_if控制過載與特化
有些工具還不夠組成它們自己的庫,因此它們與其它實體被集合到一起。這就形成了 Boost.Utility,收集了一些沒有更合適地方存放的、有用的工具。它們很有用,應該被加入到Boost,但它們又太小,不足以形成自己的庫。本文介紹Boost.Utility中最基本的以及最廣泛使用的工具。
我們將從 BOOST_STATIC_ASSERT開始,它是一個在編譯期判斷整型常量表達式的工具。然後,我們看看當你通過一個指向不完整型別的指標delete物件時,即當被刪除的物件的記憶體佈局未知時,會發生什麼。checked_delete 使得這個討論更為有趣。我們還會看到 noncopyable 如何防止一個類被複制,這也是本章最重要的主題。然後我們將看到 addressof, 它用於阻止那些過載了operator&的險惡的程式設計師的病態行為。最後,我們將測試 enable_if, 它非常有用,可用於在名字查詢時控制函式過載與模板特化是否被考慮。
BOOST_STATIC_ASSERT
標頭檔案: "boost/static_assert.hpp"
在執行期執行斷言可能是你經常用到的,也是非常合理的。它是測試前置條件、後置條件以及不變式的好方法。執行執行期斷言有很多不同的方法,但是在編譯期你如何進行斷言呢?當然,唯一的方法就是讓編譯器產生一個錯誤,這是很平常的事情(我在無意中都做過幾千次了),但如何從錯誤資訊中獲得有意義的資訊卻不是那麼明顯的。而且,即使你在一個編譯器上找到了辦法,也很難把它移植到其它編譯器上。這就是使用 BOOST_STATIC_ASSERT的原因。它可以在不同的平臺上使用,正如我們即將看到的。
用法
要開始使用靜態斷言,就要包含標頭檔案 "boost/static_assert.hpp". 該標頭檔案定義了巨集BOOST_STATIC_ASSERT. 作為它的第一個使用範例,我們來看看如何在類作用域中使用它。考慮一個泛化的類,它要求例項化時所用的型別是一個整數型別。我們不想為所有型別提供特化,因此我們需要在編譯期進行測試,以確保我們的類的確是用一個整數型別進行例項化的。現在,我們先提前一點使用另一個Boost庫來進行測試,它就是 Boost.Type_traits. 我們使用一個稱為is_integral的斷言,它對它的引數執行一個編譯期求值,正如你從它的名字可以猜到的一樣,求值的結果是表明該型別是否一個整數型別。
#include <iostream>
#include "boost/type_traits.hpp"
#include "boost/static_assert.hpp"
template <typename T> class only_compatible_with_integral_types {
BOOST_STATIC_ASSERT(boost::is_integral<T>::value);
};
有了這個斷言,在例項化類 only_compatible_with_integral_types 時如果試圖使用一個非整型的型別,就會導致一個編譯期的失敗。輸出資訊取決於編譯器,但在多數編譯器下輸出資訊會驚人地一致。
假設我們試圖這樣例項化:
- only_compatible_with_integral_types<double> test2;
Error: use of undefined type
'boost::STATIC_ASSERTION_FAILURE<false>'
在類的作用域裡,你可以明確類的要求:象在前面這樣的模板中明確引數的型別就是一個明顯的例子。你也可以使用斷言來明確類所要求的其它前提條件,如型別的大小等等。
函式作用域中的BOOST_STATIC_ASSERT
BOOST_STATIC_ASSERT 也可以用在函式作用域中。例如,考慮一個泛化的函式,它帶有一個非型別模板引數,並且該引數只接受1至10的值。與其在執行期執行斷言,我們不如在編譯器使用靜態斷言。
template <int i> void accepts_values_between_1_and_10() {
BOOST_STATIC_ASSERT(i>=1 && i<=10);
}
該函式的使用者不能使用超出允許範圍的數值來例項化這個函式。當然,斷言中的表示式必須是一個純粹的編譯期表示式,也就是說,表示式中的引數和操作符都必須被編譯器所認識。BOOST_STATIC_ASSERT 當然並不是只能用於泛型函式;我們可以在任何函式中很方便地測試條件。例如,一個函式需要一個與平臺相關的前提條件,就常常需要一個斷言。
void expects_ints_to_be_4_bytes() {
BOOST_STATIC_ASSERT(sizeof(int)==4);
}
總結
你所看到的這種靜態斷言在C++中正變得象執行期斷言assert那樣常用。這應該至少部分地歸功於"超程式設計革命",它使得一個程式中更多的計算量在編譯期執行。表達編譯期斷言的唯一方法就是讓編譯器產生一個錯誤。為了讓斷言可用,錯誤提示必須可以傳達有用的資訊,但這很難做到可移植(事實上,根本不可能做到)。這正是 BOOST_STATIC_ASSERT 所要做的,它在大多數的編譯器下提供了編譯期斷言的一致輸出。它可用於名字空間、類、函式以及作用域。
以下情形下使用 BOOST_STATIC_ASSERT :
- 當條件可以在編譯期進行求值
- 對型別的要求可以在編譯期表示
- 你需要對兩個或以上的整型常量間的關係進行斷言
checked_delete
標頭檔案: "boost/checked_delete.hpp"
通過指標來刪除一個物件時,執行的結果取決於執行刪除時被刪除的型別是否可知。對一個指向不完整型別的指標執行delete幾乎不可能有編譯器警告,這會導致各種各樣的麻煩,由於解構函式可以沒有被執行。換句話說,即進行清除的程式碼沒有被執行。checked_delete 在物件析構時執行一個靜態斷言,測試類是否可知,以確保解構函式被執行。
用法
checked_delete 是一個boost名字空間中的模板函式。它用於刪除動態分配的物件,對於動態分配的陣列,同樣有一個稱為 checked_array_delete的模板函式。這些函式接受一個引數:要刪除的指標,或是要刪除的陣列。這兩個函式都要求在銷燬物件時(即物件被傳給函式時),這些被刪除的型別必須是可知的。使用這些函式,要包含標頭檔案"boost/checked_delete.hpp". 使用這些函式時,你只需象呼叫delete那樣簡單地呼叫它們。以下程式前向聲明瞭一個類some_class, 而沒有定義它。有些編譯器允許對一個指向
some_class 的指標被刪除(稍後再討論這個),但使用 checked_delete 後,就不能通過編譯了,除非有一個 some_class 的定義。
#include "boost/checked_delete.hpp"
class some_class;
some_class* create() {
return (some_class*)0;
}
int main() {
some_class* p=create();
boost::checked_delete(p2);
}
如果你試圖編譯這段程式碼,對函式 checked_delete<some_class> 的例項化將失敗,因為 some_class 是一個不完整的型別。你的編譯器會輸出類似下面的資訊:
checked_delete.hpp: In function 'void
boost::checked_delete(T*) [with T = some_class]':
checked_sample.cpp:11: instantiated from here
boost/checked_delete.hpp:34: error: invalid application of 'sizeof' to an incomplete type
boost/checked_delete.hpp:34: error: creating array with
size zero ('-1')
boost/checked_delete.hpp:35: error: invalid application of
'sizeof' to an incomplete type
boost/checked_delete.hpp:35: error: creating array with
size zero ('-1')
boost/checked_delete.hpp:32: warning: 'x' has incomplete type
錯誤資訊的前面部分清楚地說明了問題:checked_delete 遇到了一個不完整的型別。但我們的程式碼中哪裡存在不完整的型別呢?接下來的章節我們來討論它。
究竟是什麼問題?
在我們深入瞭解 checked_delete的好處之前,讓我們先來徹底弄清楚問題所在。如果你試圖刪除一個指標,而該指標指向的是一個帶有非平凡解構函式的不完整型別,結果將是未定義的行為。這是如何發生的呢?讓我們來看一個例子。
// deleter.h
class to_be_deleted;
class deleter {
public:
void delete_it(to_be_deleted* p);
};
// deleter.cpp
#include "deleter.h"
void deleter::delete_it(to_be_deleted* p) {
delete p;
}
// to_be_deleted.h
#include <iostream>
class to_be_deleted
{
public:
~to_be_deleted() {
std::cout <<
"I'd like to say important things here, please.";
}
};
// Test application
#include "deleter.h"
#include "to_be_deleted.h"
int main() {
to_be_deleted* p=new to_be_deleted;
deleter d;
d.delete_it(p);
}
以上程式碼試圖 delete 一個指向不完整型別to_be_deleted的指標,這會導致未定義行為。注意,to_be_deleted 在 deleter.h中是前向宣告的;deleter.cpp 包含了 deleter.h 而沒有包含 to_be_deleted.h: 而to_be_deleted.h 中為to_be_deleted定義了一個非平凡解構函式。這種麻煩很容易出現,尤其是在使用智慧指標的時候。我們要做的就是在呼叫delete時確認型別是完整的,這正是 checked_delete 所做的。
checked_delete 來解決問題
前面的例子說明了刪除不完整型別時不進行確認很可能會引起麻煩,而且不是所有編譯器會對此給出警告。編寫泛型程式碼時,避免這種情況是非常必要的。使用 checked_delete重寫這個例子,你只需要把 delete p 改為 checked_delete(p).
void deleter::do_it(to_be_deleted* p) {
boost::checked_delete(p);
}
checked_delete 基本上就是一個判斷類是否完整的斷言,它的實現如下:
template< typename T > inline void checked_delete(T * x) {
typedef char type_must_be_complete[sizeof(T)];
delete x;
}
這裡的想法是建立一個char的陣列,陣列的元素數量為T的大小。如果 checked_delete 被一個不完整的型別 T 所例項化,編譯將會失敗,因為 sizeof(T) 會返回 0, 而建立一個0個元素的(自動)陣列是非法的。你也可以用 BOOST_STATIC_ASSERT 來執行這個斷言。
- BOOST_STATIC_ASSERT(sizeof(T));
- to_be_deleted* p=new to_be_deleted[10];
- boost::checked_array_delete(p);
刪除一個動態分配的物件時,必須呼叫它的解構函式。如果這個型別是不完整的,即只有宣告沒有定義,那麼解構函式可能會沒被呼叫。這是一種潛在的危險狀態,所以應該避免它。對於類模板及函式模板,風險會更大,因為無法預先知道會使用什麼型別。使用 checked_delete 和 checked_array_delete, 可以解決這個刪除不完整型別的問題。它沒有執行期的額外開銷,只是直接呼叫 delete, 因此說 checked_delete 帶來的安全性實際上是免費的。
如果你需要在呼叫delete時確保型別是完整的,就使用 checked_delete 。
noncopyable
標頭檔案: "boost/utility.hpp"
通常編譯器都是程式設計師的好朋友,但並不總是。它的好處之一在於它會自動為我們提供複製建構函式和賦值操作符,如果我們決定不自己動手去做的話。這也可能會導致一些不愉快的驚訝,如果這個類本身就不想被複制(或被賦值)。如果真是這樣,我們就需要明確地告訴這個類的使用者複製構造以及賦值是被禁止的。我不是說在程式碼中進行註釋說明,而是說要禁止對複製建構函式以及賦值操作符的訪問。幸運的是,當類帶有不能複製或不能賦值的基類或成員函式時,編譯器生成的複製建構函式及賦值操作符就不能使用。boost::noncopyable 的工作原理就是禁止訪問它的複製建構函式和賦值操作符,然後使用它作為基類。
用法
要使用 boost::noncopyable, 你要從它私有地派生出不可複製類。雖然公有繼承也可以,但這是一個壞習慣。公有繼承對於閱讀類宣告的人而言,意味著IS-A (表示派生類IS-A 基類)關係,但表明一個類IS-A noncopyable 看起來有點不太對。要從noncopyable派生,就要包含 "boost/utility.hpp" 。
#include "boost/utility.hpp"
class please_dont_make_copies : boost::noncopyable {};
int main() {
please_dont_make_copies d1;
please_dont_make_copies d2(d1);
please_dont_make_copies d3;
d3=d1;
}
這個例子不能通過編譯。由於noncopyable的複製建構函式是私有的,因此對d2進行復制構造的嘗試會失敗。同樣,由於noncopyable的賦值操作符也是私有的,因此將d1賦值給d3的嘗試也會失敗。編譯器會給出類似下面的輸出:
noncopyable.hpp: In copy constructor
' please_dont_make_copies::please_dont_make_copies (const please_dont_make_copies&)':
boost/noncopyable.hpp:27: error: '
boost::noncopyable::noncopyable(const boost::noncopyable&)' is
private
noncopyable.cpp:8: error: within this context
boost/noncopyable.hpp: In member function 'please_dont_make_copies&
please_dont_make_copies::operator=(const please_dont_make_copies&)':
boost/noncopyable.hpp:28: error: 'const boost::noncopyable&
boost::noncopyable::operator=(const boost::noncopyable&)' is private
noncopyable.cpp:10: error: within this context
下一節我們將測試這是如何工作的。很清楚從noncopyable派生將禁止複製和賦值。這也可以通過把複製建構函式和賦值操作符定義為私有的來實現。 我們來看一下怎麼樣做。
使類不能複製
再看一下類 please_dont_make_copies, 為了某些原因,它不能被複制。
class please_dont_make_copies {
public:
void do_stuff() {
std::cout <<
"Dear client, would you please refrain from copying me?";
}
};
由於編譯器生成了複製建構函式和賦值操作符,所以現在不能禁止類的複製和賦值。
please_dont_make_copies p1;
please_dont_make_copies p2(p1);
please_dont_make_copies p3;
p3=p2;
解決的方法是把複製建構函式和賦值操作符宣告為私有的或是保護的,並增加一個預設建構函式(因為編譯器不再自動生成它了)。
class please_dont_make_copies {
public:
please_dont_make_copies() {}
void do_stuff() {
std::cout <<
"Dear client, would you please refrain from copying me?";
}
private:
please_dont_make_copies(const please_dont_make_copies&);
please_dont_make_copies& operator=
(const please_dont_make_copies&);
};
這可以很好地工作,但它不能馬上清晰地告訴 please_dont_make_copies的使用者它是不能複製的。下面看一下換成 noncopyable 後,如何使得類更清楚地表明不能複製,並且也可以打更少的字。
用 noncopyable
類 boost::noncopyable 被規定為作為私有基類來使用,它可以有效地關閉複製構造和賦值操作。用前面的例子來看看使用noncopyable後代碼是什麼樣子的:
#include "boost/utility.hpp"
class please_dont_make_copies : boost::noncopyable {
public:
void do_stuff() {
std::cout << "Dear client, you just cannot copy me!";
}
};
不再需要聲明覆制建構函式或賦值操作符。由於我們是從noncopyable派生而來的,編譯器不會再生成它們了,這樣就禁止了複製和賦值。簡潔可以帶來清晰,尤其是象這樣的基本且清楚的概念。對於閱讀這段程式碼的使用者來說,馬上就清楚地知道這個類是不能複製和賦值的,因為 boost::noncopyable 在類定義的一開始就出現了。最後要提醒的一點是:你還記得類的預設訪問控制是私有的嗎?這意味著預設上繼承也是私有的。你也可以象這樣寫,來更加明確這個事實:
- class please_dont_make_copies : private boost::noncopyable {
記住 the Big Three
正如我們看到的那樣,noncopyable 為禁止類的複製和賦值提供了一個方便的辦法。但何時我們需要這樣做呢?什麼情況下我們需要自定義複製建構函式或賦值操作符?這個問題有一個通用的答案,一個幾乎總是正確的答案:無論何時你需要定義解構函式、複製建構函式、或賦值操作符三個中的任意一個,你也需要定義另外兩個。它們三者間的互動性非常重要,其中一個存在,其它的通常也都必須要有。我們假設你的一個類有一個成員是指標。你定義了一個解構函式用於正確地釋放空間,但你沒有定義複製建構函式和賦值操作符。這意味著你的程式碼中至少存在兩個潛在的危險,它們很容易被觸發。
class full_of_errors {
int* value_;
public:
full_of_errors() {
value_=new int(13);
}
~full_of_errors() {
delete value_;
}
};
使用這個類時,如果你忽視了編譯器為這個類生成的複製建構函式和賦值操作符,那麼至少有三種情況會產生錯誤。
full_of_errors f1;
full_of_errors f2(f1);
full_of_errors f3=f2;
full_of_errors f4;
f4=f3;
注意,第二行和第三行是呼叫複製建構函式的兩個等價的方法。它們都會呼叫生成的複製建構函式,雖然語法有所不同。最後一個錯誤在最後一行,賦值操作符使得同一個指標被至少兩個full_of_errors例項所刪除。正確的方法是,我們需要自己的複製建構函式和賦值操作符,因為我們定義了我們自己的解構函式。以下是正確的方法:
class not_full_of_errors {
int* value_;
public:
not_full_of_errors() {
value_=new int(13);
}
not_full_of_errors(const not_full_of_errors& other) :
value_(new int(*other.value_)) {}
not_full_of_errors& operator=
(const not_full_of_errors& other) {
*value_=*other.value_;
return *this;
}
~not_full_of_errors() {
delete value_;
}
};
所以,無論何時,一個類的the big three:複製建構函式、(虛擬)解構函式、和賦值操作符,中的任何一個被手工定義,在決定不需要定義其餘兩個之前必須認真仔細地考慮清楚。還有,如果你不想要複製,記得使用 boost::noncopyable !
總結
有很多型別需要禁止複製和賦值。但是,我們經常忽略了把這些型別的複製建構函式和賦值操作符宣告為私有的,而把責任轉嫁給了類的使用者。即使你使用了私有的複製建構函式和賦值操作符來確保它們不被複制或賦值,但是對於使用者而言這還不夠清楚。當然,編譯器會友好地提醒試圖這們做的人,但錯誤來自何處也不是清晰的。最好我們可以清晰地做到這一點,而從 noncopyable 派生就是一個清晰的宣告。當你看一眼型別的宣告就可以馬上知道了。編譯的時候,錯誤資訊總會包含名字 noncopyable. 而且它也節省了一些打字,這對於某些人而言是關鍵的因素。
以下情形下使用 noncopyable :
- 型別的複製和賦值都不被允許
- 複製和賦值的禁止應該儘可能明顯
addressof
標頭檔案: "boost/utility.hpp"
要取得一個變數的地址,我們要依賴於返回的值是否真的是這個變數的地址。但是,技術上過載operator&是有可能的,這意味著存有惡意的人可以破壞你的地址相關的程式碼。boost::addressof 被用於獲得變數的地址,不管取址操作符是否被誤用。通過使用一些靈巧的內部機制,模板函式 addressof 確保可以獲得真實的物件及其地址。
用法
為確保獲得一個物件的真實地址,你要使用 boost::addressof. 它定義在 "boost/utility.hpp". 它常用於原本要使用 operator& 的地方,它接受一個引數,該引數為要獲得地址的那個物件的引用。
#include "boost/utility.hpp"
class some_class {};
int main() {
some_class s;
some_class* p=boost::addressof(s);
}
在進一步學習如何使用 addressof的細節前,瞭解一下operator&為何以及如何不一定會返回物件的地址是非常有用的。
快速瞭解一下存有惡意的人
如果你真的,真的,真的需要過載 operator&, 或者只是想試驗一下操作符過載可能的用法,這的確很容易。當你過載 operator&時,它的語義肯定會與多數使用者(以及函式!)所期望的不同,所以千萬不要為了好玩而做這件事;除非有非常好的理由,否則不要去做它。以下有一段code-breaker程式碼:
class codebreaker {
public:
int operator&() const {
return 13;
}
};
對於這個類,任何人想獲取一個codebreaker例項的地址都會得到一個不可思議的數字13.
template <typename T> void print_address(const T& t) {
std::cout << "Address: " << (&t) << '\n';
}
int main() {
codebreaker c;
print_address(c);
}
這不難做到,但是在實際的程式碼中這樣做有沒有好的理由?也許沒有,因為除非是用在區域性的類上,否則它是不安全的。原因是,雖然獲取一個不完整型別的地址是合法的,但如果是要獲取一個帶有使用者自定義operator&的不完整型別的地址則是未定義的行為。因為我們不能保證這不會發生,所以我們最好不要過載 operator&.
迅速的解決方法
即使一個類的 operator& 被過載了,也還是有辦法獲得這個類的例項的真實地址。addressof 使用了一些幕後的巧妙方法來獲得真實的地址,而不會受任何 operator& 的欺騙。如果你把函式(print_address)改為使用 addressof, 你就可以得到以下程式碼:
template <typename T> void print_address(const T& t) {
std::cout << "&t: " << (&t) << '\n';
std::cout << "addressof(t): " << boost::addressof(t) << '\n';
}
執行時,該函式將給出如下輸出(或類似於以下的輸出,因為準確的地址值取決於你的系統).
&t: 13
addressof(t): 0012FECB13
差不多就是這樣了!如果有什麼情況讓你知道或懷疑一個類的operator&被過載了,而你又需要確保得到真實的地址(由於 operator& 被過載而變得不可信了), 你就應該使用 addressof.
總結
沒有多少有力的論點支援過載 operator&,但由於這是可能的,總有些人會這樣做。當你編寫一些需要依賴於獲得物件真實地址的程式碼時,addressof 可以幫助你確保得到真實的地址。在編寫泛型程式碼時,沒有辦法知道將會操作什麼型別,因此如果需要獲取引數化型別的地址的話,就使用 addressof.
當你需要獲得一個物件的真實地址時,使用 addressof ,不必管 operator& 的語義。
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 (匹配失敗不是錯誤),很可能現在你的臉上會有困惑的表情。編譯修改過的程式碼,我們的例子會通過編譯。short 被提升為 int, 並且第一個版本被呼叫。這種令人驚奇的行為的原因是模板版本的 some_func 不再包含在過載決議的集合內了。它被排除在內是因為,編譯器看到了這個函式的返回型別要求模板型別T 要有一個巢狀型別type ,而它知道 short 不滿足這個要求,所以它把這個函式模板從過載決議集合中刪掉了。這就是 Daveed Vandevorde 和 Nicolai Josuttis 教給我們的 SFINAE, 它意味著寧可對有問題的型別不考慮函式的過載,也不要產生一個編譯器錯誤。如果型別有一個符合條件的巢狀型別,那麼它就是過載決議集合的一部分。
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來做。如下:
#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的啟用條件:
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 :
- 你需要在把一個符合某些條件的函式加入到或排除出過載決議集合中。
- 你需要根據某個條件將一個類模板的特化版本加入到或排除出特化集合中。