## 用法
在你的程序中使用 `variant`,要包含頭文件 `"boost/variant.hpp"`。這個頭文件包含了整個庫,所以你不必知道要使用哪些單獨的特性;以后,如果你要降低相關性,可以只包含那些解決問題所要的頭文件。聲明一個 `variant` 類型時,我們必須定義一組它可以存儲的類型。最常用的辦法是使用模板參數。一個可以持有類型為 `int`, `std::string`, 或 `double` 的值的 `variant` 聲明如下。
```
boost::variant<int,std::string,double> my_first_variant;
```
當變量 `my_first_variant` 被創建時,它含有一個缺省構造的 `int`, 因為 `int` 是這個 `variant` 可以持有的類型中的第一種類型。我們也可以傳遞一個可以轉換為可用類型之一的值來初始化 `variant`.
```
boost::variant<int,std::string,double>
my_first_variant("Hello world");
```
我們可以隨時賦給新的值,只要這個新值有確定的類型并且可以轉換為 `variant` 可以持有的類型中的某一種,它可以很好地工作。
```
my_first_variant=24;
my_first_variant=2.52;
my_first_variant="Fabulous!";
my_first_variant=0;
```
在第一個賦值后,所含值的類型為 `int`; 第二個賦值后,類型為 `double`; 第三個后,類型為 `std::string`; 最后,又變回 `int`. 如果我們想看看,我們可以用函數 `boost::get` 取出這個值,如下:
```
assert(boost::get<int>(my_first_variant)==0);
```
注意,如果調用 `get` 失敗(當 `my_first_variant` 所含值不是類型 `int` 時就會發生),會拋出一個類型為 `boost::bad_get` 的異常。為了避免在失敗時得到一個異常,我們可以傳給 `get` 一個 `variant` 指針,這樣 `get` 將返回一個指向它所含值的指針,或者如果給定類型與 `variant` 的值的類型不符則返回空指針。以下是它的用法:
```
int* val=boost::get<int>(&my_first_variant);
assert(val && (*val)==0);
```
函數 `get` 是訪問所含值的一種直接方法,事實上它與 `boost::any` 的 `any_cast` 很相似。注意,類型必須完全符合,包括相同的 cv-限定符(`const` 和 `volatile`)。但是,可以使用限制更多的 cv-限定符。如果類型不匹配且傳給 `get` 的是一個 `variant` 指針,將返回空指針。否則,拋出一個類型為 `bad_get` 的異常。
```
const int& i=boost::get<const int>(my_first_variant);
```
過分依賴于 `get` 的代碼很容易變得脆弱;如果我們不知道所含值的類型,我們可能會想測試所有可能的組合,就如下面這個例子的做法。
```
#include <iostream>
#include <string>
#include "boost/variant.hpp"
template <typename V> void print(V& v) {
if (int* pi=boost::get<int>(&v))
std::cout << "It's an int: " << *pi << '\n';
else if (std::string* ps=boost::get<std::string>(&v))
std::cout << "It's a std::string: " << *ps << '\n';
else if (double* pd=boost::get<double>(&v))
std::cout << "It's a double: " << *pd << '\n';
std::cout << "My work here is done!\n";
}
int main() {
boost::variant<int,std::string,double>
my_first_variant("Hello there!");
print(my_first_variant);
my_first_variant=12;
print(my_first_variant);
my_first_variant=1.1;
print(my_first_variant);
}
```
函數 `print` 現在可以正確工作,但如果我們決定改變 `variant` 的類型組的話會怎樣?我們將引入一個微妙的bug,而不能在編譯期捉住它;函數 `print` 不能打印任何其它我們沒有預先想到的類型的值。如果我們沒有使用模板函數,而是要求一個明確的 `variant` 類型,我們就要為不同類型的 `variant` 重載多個相同功能的函數。下一節將討論訪問 `variant` 的概念,以及這種(類型安全的)訪問機制解決的問題。
### 訪問Variants
讓我們從一個例子開始,它解釋了為什么使用 `get` 并沒有你想要的那么可靠。從前面父子的代碼開始,我們來修改一下 `variant` 可以包含的類型,并對 `variant` 的一個 `char` 值來調用 `print` 。
```
int main() {
boost::variant<int,std::string,double,char>
my_first_variant("Hello there!");
print(my_first_variant);
my_first_variant=12;
print(my_first_variant);
my_first_variant=1.1;
print(my_first_variant);
my_first_variant='a';
print(my_first_variant);
}
```
雖然我們給 `variant` 的類型組增加了 `char` ,并且程序的最后兩行設置了一個 `char` 值并調用 `print` ,編譯器也不會有意見 (注意,`print` 是以 `variant` 的類型來特化的,所以它可以很容易適應新的 `variant` 定義)。以下是這個程序的運行結果:
```
It's a std::string: Hello there!
My work here is done!
It's an int: 12
My work here is done!
It's a double: 1.1
My work here is done!
My work here is done!
```
這個輸出顯示了一個問題。最后一個"My work here is done!"之前沒有值的報告。原因是很簡單,`print` 不能輸出除了它原來設計好的那些類型(`std::string`, `int`, 和 `double`)以外的任何值,但它可以干凈地編譯和運行。如果 `variant` 的當前類型不被 `print` 支持,它的值就會被簡單地忽略掉。使用 `get` 還有更多潛在的問題,例如 if 語句的順序要與類的層次相一致。注意,這并不是說你應該完全避免使用 `get`;它只是說有些時候它不是最好的方法。有一種更好的機制,可以允許我們規定哪些類型的值可以接受,并且這些規定是在編譯期生效的。這是就 `variant` 訪問機制的作用。通過把一個訪問器應用到 `variant`,編譯器可以保證它們完全兼容。Boost.Variant 中這些訪問器是帶有一些函數調用操作符的函數對象,這些函數調用操作符接受與它們所訪問的 `variant` 可以包含的類型組相對應的參數。
現在我們用訪問器來重寫那個聲名狼籍的函數 `print` ,如下:
```
class print_visitor : public boost::static_visitor<void> {
public:
void operator()(int i) const {
std::cout << "It's an int: " << i << '\n';
}
void operator()(std::string s) const {
std::cout << "It's a std::string: " << s << '\n';
}
void operator()(double d) const {
std::cout << "It's a double: " << d << '\n';
}
};
```
要讓 `print_visitor` 成為 `variant` 的一個訪問器,我們要讓它派生自 `boost::static_visitor` 以獲得正確的 `typedef` (`result_type`), 并明確地聲明這個類是一個訪問器類型。這個類實現了三個重載版本的函數調用操作符,分別接受一個 `int`, 一個 `std::string`, 和一個 `double` 。為了訪問 `variant`, 你要用函數 `boost::apply_visitor`(visitor, variant). 如果我們用對 `apply_visitor`的調用來替換前面的 `print` 調用,我們可以得到如下代碼:
```
int main() {
boost::variant<int,std::string,double,char>
my_first_variant("Hello there!");
print_visitor v;
boost::apply_visitor(v,my_first_variant);
my_first_variant=12;
boost::apply_visitor(v,my_first_variant);
my_first_variant=1.1;
boost::apply_visitor(v,my_first_variant);
my_first_variant='a';
boost::apply_visitor(v,my_first_variant);
}
```
這里,我們創建了一個 `print_visitor`, 名為 `v`, 并把它應用于賦值后的 `my_first_ variant` 。因為我們沒有一個函數調用操作符接受 `char`, 這段代碼會編譯失敗,是嗎?錯!一個 `char` 可以轉換為一個 `int`, 所以這個訪問器可以兼容我們的 `variant` 類型。以下是程序運行的結果。
```
It's a std::string: Hello there!
It's an int: 12
It's a double: 1.1
It's an int: 97
```
這里我們可以學到兩件事情:第一個是字母 `a` 的 ASCII 碼值為 97, 更重要的是第二個,如果一個訪問器以傳值的方式傳遞參數,則傳送的值可以應用隱式轉換。如果我們想訪問器只能使用精確的類型(同時也避免拷貝從 `variant` 得到的值),我們必須修改訪問器的調用操作符傳遞參數的方式。以下這個版本的 `print_visitor` 只能使用類型 `int`, `std::string`, 和 `double`; 以及可以隱式轉換到這些類型的引用的其它類型。
```
class print_visitor : public boost::static_visitor<void> {
public:
void operator()(int& i) const {
std::cout << "It's an int: " << i << '\n';
}
void operator()(std::string& s) const {
std::cout << "It's a std::string: " << s << '\n';
}
void operator()(double& d) const {
std::cout << "It's a double: " << d << '\n';
}
};
```
如果再編譯一下這個程序,編譯器就不高興了,它會輸出如下信息:
```
c:/boost_cvs/boost/boost/variant/variant.hpp:
In member function `typename Visitor::result_type boost::detail:: variant::
invoke_visitor<Visitor>::internal_visit(T&, int)
[with T = char, Visitor = print_visitor]':
[Snipped lines of irrelevant information here]
c:/boost_cvs/boost/boost/variant/variant.hpp:807:
error: no match for call to `(print_visitor) (char&)'
variant_sample1.cpp:40: error: candidates are:
void print_visitor::operator()(int&) const
variant_sample1.cpp:44: error:
void print_visitor::operator()(std::string&) const
variant_sample1.cpp:48: error:
void print_visitor::operator()(double&) const
```
這個錯誤指出了問題:沒有一個候選的函數接受 `char` 參數!為什么說類型安全的編譯期訪問機制是一個強大的機制,這正是一個重要的原因。它使得訪問機制強烈依賴于類型,避免了討厭的類型變換。創建訪問器與創建其它函數對象一樣容易,因此學習曲線并不陡峭。當 `variant` 中的類型組可能會改變時(它們總是傾向于變化!),創建訪問器要比單單依賴 `get` 更可靠。雖然開始需要更高的代價,但絕對是值得的。
### 泛型訪問器
通過使用訪問器機制和泛型的調用操作符,可以創建能夠接受任意類型的泛型訪問器(無論是在語法上還是語義上,都可以實現泛型調用操作符)。這對于統一地處理不同的類型非常有用。C++的操作符就是"通用"性的一個典型例子,如算術和IO流的位移操作符。以下例子使用 `operator<<` 來輸出 `variant` 的值到一個流。
```
#include <iostream>
#include <sstream>
#include <string>
#include <sstream>
#include "boost/variant.hpp"
class stream_output_visitor :
public boost::static_visitor<void> {
std::ostream& os_;
public:
stream_output_visitor(std::ostream& os) : os_(os) {}
template <typename T> void operator()(T& t) const {
os_ << t << '\n';
}
};
int main() {
boost::variant<int,std::string> var;
var=100;
boost::apply_visitor(stream_output_visitor(std::cout),var);
var="One hundred";
boost::apply_visitor(stream_output_visitor(std::cout),var);
}
```
主要思想是 `stream_output_visitor` 中的調用操作符是一個成員函數模板,它在訪問每一種類型(本例中是 `int` 和 `std::string`)時分別實例化。因為 `std::cout << 100` 和 `std::cout << std::string("One hundred")` 都已經有定義了,所以這段代碼可以編譯并工作良好。
當然,操作符僅是可以使用泛型訪問器的一個例子;它們常常應用于更多的類型。在某些值上調用函數,或 者將它們作為參數傳給其它的函數時,要求就是對于所有傳給操作符的類型都要有相應的成員函數存在,并且對于被調用的函數要有合適的重載。這種泛型調用操作 符的另一個有趣的方面是,可以對某些類型特化其行為,但對于其余類型則仍允許泛型的實現。在某種意義上,這涉及到模板特化,即基于類型信息的行為特殊化。
### 二元訪問器
我們前面看到的訪問器都是一元的,即它們只接受一個 `variant` 作為唯一的參數。二元訪問器接受兩個(可能是不同的) `variant`. 這種概念對于實現兩個 `variant` 間的關系很有用。作為例子,我們為 `variant` 類型將創建一個按字典順序的排序。為此,我們使用一個來自于標準庫的非常有用的組件:`std::ostringstream`. 它接受任意可流輸出的東西,并且在需要時產生一個獨立的 `std::string` 。我們從而可以按字典序比較完全不同的 `variant` 類型,只要假設所有限定的類型都支持流輸出。和普通的訪問器一樣,二元訪問器也派生自 `boost::static_visitor`, 并且用模板參數表示調用操作符的返回類型。因為我們是創建一個謂詞,因此返回類型為 `bool`. 以下是一個我們即將用到的二元謂詞。
```
class lexicographical_visitor :
public boost::static_visitor<bool> {
public:
template <typename LHS,typename RHS>
bool operator()(const LHS& lhs,const RHS& rhs) const {
return get_string(lhs)<get_string(rhs);
}
private:
template <typename T> static std::string
get_string(const T& t) {
std::ostringstream s;
s << t;
return s.str();
}
static const std::string& get_string(const std::string& s) {
return s;
}
};
```
這里的調用操作符泛化了它的兩個參數,這意味著它接受任意兩種類型的組合。對于 `variant` 的可用類型組的要求就是它們必須是可流輸出(OutputStreamable)的。成員函數模板 `get_string` 使用一個 `std::ostringstream` 來把它的參數轉換為字符串表示,所以要求參數必須是可流輸出的(為了使用 `std::ostringstream`, 記得要包含頭文件 `<sstream>`)。成員函數 `get_string` 針對類型為 `std::string` 的參數進行特化,由于類型已經符合要求,所以它跳過了 `std::ostringstream` 而直接返回它的參數。在兩個參數都轉為 `std::string` 以后,剩下的就是使用 `operator<` 來比較它們了。現在我們把這個訪問器放入測試代碼,來對一個容器中的元素進行排序(我們還將重用我們在本章前面創建的 `stream_output_visitor`?)。
```
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include "boost/variant.hpp"
int main() {
boost::variant<int,std::string> var1="100";
boost::variant<double> var2=99.99;
std::cout << "var1<var2: " <<
boost::apply_visitor(
lexicographical_visitor(),var1,var2) << '\n';
typedef std::vector<
boost::variant<int,std::string,double> > vec_type;
vec_type vec;
vec.push_back("Hello");
vec.push_back(12);
vec.push_back(1.12);
vec.push_back("0");
stream_output_visitor sv(std::cout);
std::for_each(vec.begin(),vec.end(),sv);
lexicographical_visitor lv;
std::sort(vec.begin(),vec.end(),boost::apply_visitor(lv));
std::cout << '\n';
std::for_each(vec.begin(),vec.end(),sv);
};
```
首先,我們將訪問應用于兩個 `variants`, `var1` 和 `var2`, 如下:
```
boost::apply_visitor(lexicographical_visitor(),var1,var2)
```
如你所見,與一元訪問器不同的是,有兩個 `variant` 被傳遞給函數 `apply_visitor`. 一個更為常見的用例是使用這個謂詞來對元素進行排序,我們這樣來做:
```
lexicographical_visitor lv;
std::sort(vec.begin(),vec.end(),boost::apply_visitor(lv));
```
當 `sort` 算法被執行時,它使用我們傳入的謂詞來比較它的元素,它是一個 `lexicographical_visitor` 實例。注意,`boost::variant` 已經定義了 `operator<`, 所以不使用謂詞也可以對容器進行排序。
```
std::sort(vec.begin(),vec.end());
```
但是這種缺省的排序是首先使用 `which` 來檢查當前值的索引,所以元素的排列順序將是 12, 0, Hello, 1.12, 而我們想要的是按字典序來排序。因為 `variant` 類已經提供了 `operator<` 和 `operator==` ,所以 `variant` 可以用作所有標準庫容器的元素類型。當缺省的關系比較不夠用時,你需要用二元訪問器來實現一個。
### 更多應該知道的事情
我們并沒有涉及到 Boost.Variant 庫的所有功能。其它更為先進的特性不如我們已經提到的那么常用。但是,我會簡要地說一下,因此你將至少知道在需要時可以找到哪些可用的東西。宏 `BOOST_VARIANT_ENUM_PARAMS`, 可用于為 `variant` 類型重載/特化函數和類模板。這個宏用于列舉 `variant` 可以包含的類型組。還有支持使用類型序列來創建 `variant` 類型,即通過 `make_variant_over?`編譯期列表來表示 `variant` 的類型組。還有遞歸的 `variant` 類型,可用于創建它們自己類型的表達式,遞歸 `variant` 類型使用 `recursive_wrapper`, `make_recursive_variant`, 和 `make_recursive_variant_over`. 如果你需要這些額外的特性,在線文檔可以很好地解釋它們。
- 序
- 前言
- 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 總結