C++ Boost

Boost Tuple 庫

所謂tuple (又稱為 N元組) 是一個由固定數目個元素組成的集合。 例如pairs, triples, quadruples 等都是tuple。在編程語言中 ,tuple是指一個包含若干對象的數據對象,包含的對象稱為元素。其中每個元素可以是不同類型的對象。

Tuple在很多場合下都能方便使用。 例如,借助tuple能輕易地定義一個返回多個值的函數。

在某些編程語言中,例如ML,Python,Haskell,含有作為內建類型的tuple類型。 不幸的是,在C++中沒有這樣的內建類型。為了擬補C++這個“缺點”,Boost Tuple庫運用模板技術實現出一個tuple類型。

目錄

  1. 使用這個庫
  2. Tuple型別
  3. 構件tuple
  4. 訪問tuple元素
  5. 拷貝構造與賦值
  6. 關係操作符
  7. Tiers 連結
  8. 串行化
  9. 性能
  10. 可移植性
  11. 感謝
  12. 參考引用

更多細節

高級特性 (描述一些元函數和相關內容).

設計邏輯與實現

使用這個庫

若要使用這個庫,只需要包含:

#include "boost/tuple/tuple.hpp"

要使用比較操作符,請包含:

#include "boost/tuple/tuple_comparison.hpp"

要使用tuple的輸入輸出操作符,請,

#include "boost/tuple/tuple_io.hpp"
其中,tuple_io.hpptuple_comparison.hpp 都已經包含有tuple.hpp。

大部分定義都在命名空間 ::boost::tuples,除了最常用的一些功能放在 ::boost 。 最常用的是: tuplemake_tupletieget。 還有,輔助用的 refcref::boost 中定義.。

Tuple 型別

所謂tuple型別是tuple模板的一個實例。模板參數是元素的類型。目前版本支持包含0到10個元素的tuple。如果你需要更多個元素,最多上限可支持幾十個元素。 可支持任意的C++類型。注意 void 類型和普通函數類型是合法的C++類型, 但是這些類型不能生成對象。 因此,包含這個類型的tuple型別可存在,但不能產生tuple對象。顯然可見,那些不能被拷貝的,沒有默認構造函數的類型,當用作tuple元素型別的時候,也會受到相應的限制 (詳見下節 '構件tuple' )。

例如,下面是合法的tuple實例定義 (其中ABC 是用戶定義類型):

tuple<int>
tuple<double&, const double&, const double, double*, const double*>
tuple<A, int(*)(char, int), B(A::*)(C&), C>
tuple<std::string, std::pair<A, B> >
tuple<A*, tuple<const A*, const B&, C>, bool, void*>

構件tuple

Tuple的構造函數以其元素作為參數。 對於一個n個元素的tuple,構造函數能接受k 個參數,其中 0 <= k <= n 例如:

tuple<int, double>() 
tuple<int, double>(1)
tuple<int, double>(1, 3.14)

對於沒有初始值的元素,就自動被默認構造(因此該元素類型必須有默認構造函數)。例如:

class X {
X();
public:
X(std::string);
};

tuple<X,X,X>() // 錯誤: X類型沒有實現默認構造函數
tuple<X,X,X>(string("Jaba"), string("Daba"), string("Duu")) // 正確
特別地,引用類型不能默認構造,必須提供初始值:
tuple<double&>()                // 錯誤: 引用必須 
// 顯式初始化

double d = 5;
tuple<double&>(d) // 正確

tuple<double&>(d+3.14) // 錯誤: 不能用臨時對像
// 初始化非const的引用

tuple<const double&>(d+3.14) // 可以,不過很危險:
// 該元素成為懸空引用

對不能拷貝構造的元素提供初始值,會引起編譯錯誤

class Y { 
Y(const Y&);
public:
Y();
};

char a[10];

tuple<char[10], Y>(a, Y()); // 錯誤, 數組類型和Y類型不能拷貝構造
tuple<char[10], Y>(); // 正確
特別注意,下面的語句是完全正確的:
Y y;
tuple<char(&)[10], Y&>(a, y);
可能會出現一種無法構造的tuple型別。 這種情況出現在一個不能初始化的元素先於一個必須初始化的元素出現在tuple元素列表。例: tuple<char[10], int&>.

總體上,一個tuple的構造過程語義上相當於一組獨立的元素構造過程。

關於 make_tuple 函數

Tuple也可以通過函數make_tuple構造(類似於構造pair的函數std::make_pair)。這樣將更方便地構造tuple,省卻了顯式指定元素的類型:

tuple<int, int, double> add_multiply_divide(int a, int b) {
return make_tuple(a+b, a*b, double(a)/double(b));
}

缺省情況下,元素類型被自動推導為普通的非引用類型,例:

void foo(const A& a, B& b) { 
...
make_tuple(a, b);
這裡調用 make_tuple 得到的結果類型是tuple<A, B>

有時這種缺省的普通非引用類型不是我們需要的,例如當元素類型不可拷貝構造時。 因此,程序員可以去控制類型推導,把元素類型聲明為一個常量或非常量引用。 這需要在兩個模板函數的幫助下實現 refcref 任意參數都可以被這兩個函數包裝為需要的類型。 其實現技術上也能保證對象的常量性不被破壞,當常量對像經過ref包裝後將成為常量引用,而不是非常量引用(下面代碼第五行)。例如:

A a; B b; const A ca = a;
make_tuple(cref(a), b); // 產生類型 tuple<const A&, B>
make_tuple(ref(a), b); //
產生類型 tuple<A&, B>
make_tuple(ref(a), cref(b)); //
產生類型 tuple<A&, const B&>
make_tuple(cref(ca)); //
產生類型 tuple<const A&>
make_tuple(ref(ca)); //
產生類型 tuple<const A&>

在make_tuple中,數組參數被默認推導為常量引用,不需要cref進行包裝,例如:

make_tuple("Donald", "Daisy");
這樣產生的tuple對象的類型是 tuple<const char (&)[7], const char (&)[6]> (注意,字符串字面常量的類型是常量字符的數組,而不是 const char*)。但是,如果要用函數 make_tuple 產生非常量數組類型,就要用ref包裝了。

函數指針被推導為普通的非引用類型,也就是普通的函數指針。 Tuple元素也可以是一個函數的引用類型,但是這種tuple不能通過make_tuple構造。(一個普通的函數類型會是推導的結果,是非法的):

void f(int i);
...
make_tuple(&f); // tuple<void (*)(int)>
...
tuple<tuple<void (&)(int)> > a(f) // 可以
make_tuple(f); // 錯誤

訪問 tuple 元素

通過下面的表達式訪問tuple元素:

t.get<N>()
或者
get<N>(t)
其中 t 是一個tuple對象, N 是一個指定被訪問元素下標的常數表達式。 根據 t 是否常量, get 函數返回的第 N 個元素的類型自動成為常量引用或非常量引用。 首元素的下標為 0 ,因此 N 必須在 0 到 k-1 之間,其中 k 是tuple中元素的個數。 違反這個約定將會導致編譯錯誤。 例如:
double d = 2.7; A a;
tuple<int, double&, const A&> t(1, d, a);
const tuple<int, double&, const A&> ct = t;
...
int i = get<0>(t); i = t.get<0>(); // 正確
int j = get<0>(ct); // 正確
get<0>(t) = 5; // 正確
get<0>(ct) = 5; // 錯誤, 不能對常量賦值
...
double e = get<1>(t); // 正確
get<1>(t) = 3.14; // 正確
get<2>(t) = A(); // 錯誤,不能對常量賦值
A aa = get<3>(t); // 錯誤:下標超出範圍
...
++get<0>(t); // 正確,可以像普通變量一樣使用
注意! 這個 get 成員函數在微軟的Visual C++編譯器下不支持。 還有,若不顯式地使用命名空間前綴,編譯器將找不到非成員get函數。 因此,當用 MSVC++ 6.0 編譯時,所有的get調用都應寫成 tuples::get<N>(a_tuple) 。

拷貝構造與賦值

一個 tuple對象可以通過另一個tuple對像拷貝構造, 假若每一個元素都是逐一可拷貝構造的。 類似地,一個tuple對象可以對另一個tuple對像賦值,假若每一個元素都是逐一可賦值的。例如:

class A {};
class B : public A {};
struct C { C(); C(const B&); };
struct D { operator C() const; };
tuple<char, B*, B, D> t;
...
tuple<int, A*, C, C> a(t); // 正確
a = t; // 正確
這兩種情況進行如下類型轉換,char -> int, B* -> A* (派生類指針轉為基類指針), B -> C (用戶定義轉換) and D -> C (用戶定義轉換)。

注意,庫也定義了從一個std::pair到相應的tuple類型的賦值:

tuple<float, int> a = std::make_pair(1, 'a');

關係操作符

作用在tuple上的關係操作符 ==, !=, <, >, <= , >= 會轉發到tuple內的元素的相應操作符上。 意思是,對於某一個關係操作符,如果在所有的元素上都有定義,則該操作符將同樣地在整個tuple上有定義。 Tuple的相等操作符的定義是

操作符 <, >, <= , >= 通過字典順序實現。

要注意,若對元素個數不同的兩個tuple進行比較會產生編譯錯誤。

還有,比較操作符是“短路”的:比較從第一個元素開始,一旦能得出結果就馬上停止,忽略後面元素的比較運算。

Examples:

tuple<std::string, int, A> t1(std::string("same?"), 2, A());
tuple<std::string, long, A> t2(std::string("same?"), 2, A());
tuple<std::string, long, A> t3(std::string("different"), 3, A());

bool operator==(A, A) { std::cout << "All the same to me..."; return true; }

t1 == t2; // true
t1 == t3; // false, 沒有打印出 "All the..."

Tiers連結

Tiers 是一種 tuple, 其所有的元素都是非常量引用類型。 它通過調用tie函數構造 (類比 make_tuple):

int i; char c; double d; 
...
tie(i, c, a);

上面 tie 函數生成了一個類型為 tuple<int&, char&, double&>的tuple 。 也可調用 make_tuple(ref(i), ref(c), ref(a))得到同樣結果。

一個包含非常量引用元素的tuple可以使另一個tuple“解包”為普通變量。例如:

int i; char c; double d; 
tie(i, c, d) = make_tuple(1,'a', 5.5);
std::cout << i << " " << c << " " << d;
這段代碼把1 a 5.5打印到標準輸出流。 在ML和Python語言中,這樣一個tuple解包操作被作為一個例子。 調用返回tuple的函數是很方便的。

這種tiers連結技術也可以用在 std::pair 上:

int i; char c;
tie(i, c) = std::make_pair(1, 'a');

ignore對像

有一個特殊的對象叫“ignore對像”它允許你在賦值時忽略某些元素。 如果函數返回的tuple中,你只對其中一部分元素感興趣,則可運用到“ignore對像”。例如(注意,ignore對像在子命名空間tuples):
char c;
tie(tuples::ignore, c) = std::make_pair(1, 'a');

串行化

Tuple 向 std::ostream 的輸出的全局操作符 operator<< 被重載,定義為對tuple的元素逐一調用 operator<<  。

類似地,為了從輸入流 std::istream 得一個tuple,全局操作符 operator>> 也被重載,定義為對元素逐一調用 operator>>  。

元素之間默認的分隔符是空格,而且整個tuple用圓括號包圍。例如:

tuple<float, int, std::string> a(1.0f,  2, std::string("Howdy folks!");

cout << a;
輸出tuple是: (1.0 2 Howdy folks!)

這個庫定義了三個函數,用於改變默認的行為:

注意,這些函數在子命名空間tuples中定義。例如:
cout << tuples::set_open('[') << tuples::set_close(']') << tuples::set_delimiter(',') << a; 
輸出剛才同樣的tuple a如此: [1.0,2,Howdy folks!]

這三個函數也適用於輸入操作符 operator>>istream 。 假設標準輸入流 cin 流有如下數據:

(1 2 3) [4:5]
程序代碼:
tuple<int, int, int> i;
tuple<int, int> j;

cin >> i;
cin >> tuples::set_open('[') >> tuples::set_close(']') >> tules::set_delimiter(':');
cin >> j;
把數據讀入tuple i 和 j  。

注意,從std::string 或者 C風格的字符串中提取出tuple不一定行得通,因為解析可串行化的tuple時可能帶有歧義。

性能

所有的tuple訪問和構造函數都是很小的只有一行的內聯函數。 因此,一個好的編譯器可以消除任何額外的開銷,產生相當於手寫tuple(例如用類的方法)一樣高質量的代碼。 特別地,若用好的編譯器,下面的代碼沒有性能差別:
class hand_made_tuple { 
A a; B b; C c;
public:
hand_made_tuple(const A& aa, const B& bb, const C& cc)
: a(aa), b(bb), c(cc) {};
A& getA() { return a; };
B& getB() { return b; };
C& getC() { return c; };
};

hand_made_tuple hmt(A(), B(), C());
hmt.getA(); hmt.getB(); hmt.getC();
和同樣功能的代碼:
tuple<A, B, C> t(A(), B(), C());
t.get<0>(); t.get<1>(); t.get<2>();

注意,有一些廣泛使用的編譯器(例如bcc 5.5.1)不能夠優化這種tuple用法。

根據編譯器的優化能力,在函數返回多個值的情況下,對比直接用非常量引用參數的方法,使用tier連結技術會有很小的性能下降。 例如,假設如下函數 f1f2 有同樣的功能:

void f1(int&, double&);
tuple<int, double> f2();
那麼按照如下代碼,調用f1稍微比f2快一點:
int i; double d;
...
f1(i,d); // #1
tie(i,d) = f2(); // #2
參閱 [1, 2] 有更多深入的關於效率的討論。

對編譯時間的影響

因為過多的模板實例化,編譯tuple會比較慢。 根據編譯器和tuple長度的不同,編譯一個tuple比編譯一個等效的類(例如上述的“手寫tuple”類)可能需要多花10倍的時間。 但是,一個實際的程序除了tuple的定義之外,還包含很多實現其他程序功能的代碼,使用tuple而多花費的時間可能是察覺不出的。有人曾測量過, 在頻繁使用tuple的程序中,編譯時間增加5到10個百分點。 在該的測試程序中,編譯需要的內存增加22%到27%。 參閱 [1, 2] 有更詳細內容。

可移植性

這個庫的代碼是用標準C++編寫,所以可用於和C++標準相容的編譯器。 下面是此庫在一些流行編譯器上的已知問題:

Compiler Problems
gcc 2.95 -
edg 2.44 -
Borland 5.5 不能用函數指針或者成員指針作為元素類型
Metrowerks 6.2 不能用 refcref 包裝
MS Visual C++ 沒有引用 (tie 仍然可用)。 不能用 refcref 包裝

感謝

Gary Powell 是一個不可缺少的幫手。 特別地,tuple的流操作是他設計出來的。 Doug Gregor 提出一個可在 MSVC編譯的工作版本, David Abrahams 找到一個避開編譯器不支持偏特化大部分約束的方法。 感謝 Jeremy Siek, William Kempf 和 Jens Maurer ,他們提供的幫助和提出的建議。 Vesa Karvonen, John Max Skaller, Ed Brey, Beman Dawes, David Abrahams 和 Hartmut Kaiser 對庫提出有建設性的意見。 tie連結的思路來自於一個很久以前在usenet上的文章,作者是Ian McCulloch,他提出了一些類似std::pairs的設計。

參考引用

[1] Järvi J.: Tuples and multiple return values in C++, TUCS Technical Report No 249, 1999.

[2] Järvi J.: ML-Style Tuple Assignment in Standard C++ - Extending the Multiple Return Value Formalism, TUCS Technical Report No 267, 1999.

[3] Järvi J.:Tuple Types and Multiple Return Values, C/C++ Users Journal, August 2001.


Last modified 2003-09-07

© Copyright Jaakko Järvi 2001. Permission to copy, use, modify, sell and distribute this software and its documentation is granted provided this copyright notice appears in all copies. This software and its documentation is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose.