所谓tuple (又称为 N元组) 是一个由固定数目个元素组成的集合。 例如pairs, triples, quadruples 等都是tuple。在编程语言中 ,tuple是指一个包含若干对象的数据对象,包含的对象称为元素。其中每个元素可以是不同类型的对象。
Tuple在很多场合下都能方便使用。 例如,借助tuple能轻易地定义一个返回多个值的函数。
在某些编程语言中,例如ML,Python,Haskell,含有作为内建类型的tuple类型。不幸的是,在C++中没有这样的内建类型。为了拟补C++这个“缺点”,Boost Tuple库运用模板技术实现出一个tuple类型。
使用这个库
若要使用这个库,只需要包含:
#include "boost/tuple/tuple.hpp"
要使用比较操作符,请包含:
#include "boost/tuple/tuple_comparison.hpp"
要使用tuple的输入输出操作符,请,
#include "boost/tuple/tuple_io.hpp"
其中,tuple_io.hpp 和 tuple_comparison.hpp 都已经包含有tuple.hpp。
大部分定义都在命名空间 ::boost::tuples,除了最常用的一些功能放在 ::boost 。 最常用的是: tuple, make_tuple, tie 和 get。 还有,辅助用的 ref 和 cref 在 ::boost 中定义.。
Tuple 型别
所谓tuple型别是tuple模板的一个实例。模板参数是元素的类型。目前版本支持包含0到10个元素的tuple。如果你需要更多个元素,最多上限可支持几十个元素。可支持任意的C++类型。注意 void 类型和普通函数类型是合法的C++类型, 但是这些类型不能生成对象。 因此,包含这个类型的tuple型别可存在,但不能产生tuple对象。显然可见,那些不能被拷贝的,没有默认构造函数的类型,当用作tuple元素型别的时候,也会受到相应的限制 (详见下节 '构件tuple' )。
例如,下面是合法的tuple实例定义 (其中A,B ,C 是用户定义类型):
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>
有时这种缺省的普通非引用类型不是我们需要的,例如当元素类型不可拷贝构造时。 因此,程序员可以去控制类型推导,把元素类型声明为一个常量或非常量引用。这需要在两个模板函数的帮助下实现 ref 和 cref 。 任意参数都可以被这两个函数包装为需要的类型。 其实现技术上也能保证对象的常量性不被破坏,当常量对象经过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的相等操作符的定义是:
a == b iff for each i: ai == bi
a != b iff exists i: ai != bi
操作符 <, >, <= , >= 通过字典顺序实现。
要注意,若对元素个数不同的两个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; // truet1 == 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!)
这个库定义了三个函数,用于改变默认的行为:
set_open(char) 设置第一个元素之前输出的字符。
set_close(char) 设置最后一个元素之后输出的字符。
set_delimiter(char) 设置元素之间分隔符。
注意,这些函数在子命名空间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连结技术会有很小的性能下降。 例如,假设如下函数 f1 和 f2 有同样的功能:
void f1(int&, double&);tuple<int, double> f2();
那么按照如下代码,调用f1稍微比f2快一点:
int i; double d; ...f1(i,d); // #1tie(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 不能用 ref 和 cref 包装
MS Visual C++ 没有引用 (tie 仍然可用)。 不能用 ref 和 cref 包装