Boost C++ 库

...世界上最受推崇和专业设计的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

Next

第 1 章。 Boost.Tuple

根据 Boost 软件许可,1.0 版 分发。

目录

Tuple 库高级功能
设计决策原理
使用库
Tuple 类型
构造 Tuples
访问 Tuple 元素
复制构造和 Tuple 赋值
关系运算符
Tiers
性能
可移植性
更多细节
致谢
参考文献

tuple(或 n-tuple)是固定大小的元素集合。对、三元组、四元组等都是 tuples。在编程语言中,tuple 是一个数据对象,包含其他对象作为元素。这些元素对象可以是不同的类型。

在许多情况下,Tuples 都很方便。例如,tuples 使定义返回多个值的函数变得容易。

一些编程语言,如 ML、Python 和 Haskell,具有内置的 tuple 构造。不幸的是,C++ 没有。为了弥补这种“缺陷”,Boost Tuple 库使用模板实现了一个 tuple 构造。

要使用该库,只需包含

#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 中,但最常用的名称使用 using 声明提升到命名空间 ::boost。这些名称是:tuplemake_tupletieget。此外,refcref 直接在 ::boost 命名空间下定义。

tuple 类型是 tuple 模板的实例化。模板参数指定 tuple 元素的类型。当前版本支持 0-10 个元素的 tuples。如果需要,上限可以增加到,例如,几十个元素。数据元素可以是任何 C++ 类型。请注意,void 和普通函数类型是有效的 C++ 类型,但此类类型的对象不存在。因此,如果 tuple 类型包含此类类型作为元素,则 tuple 类型可以存在,但该类型的对象不能存在。对于无法复制或非默认可构造的元素类型存在自然限制(请参阅下面的“构造 tuples”)。

例如,以下定义是有效的 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>()                                              // error: no default constructor for X
tuple<X,X,X>(string("Jaba"), string("Daba"), string("Duu")) // ok

特别是,引用类型没有默认初始化

tuple<double&>()                // error: reference must be
                                // initialized explicitly

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

tuple<double&>(d+3.14)          // error: cannot initialize
                                // non-const reference with a temporary

tuple<const double&>(d+3.14)    // ok, but dangerous:
                                // the element becomes a dangling reference

对无法复制的元素使用初始值是编译时错误

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

char a[10];

tuple<char[10], Y>(a, Y()); // error, neither arrays nor Y can be copied
tuple<char[10], Y>();       // ok

特别注意,以下内容完全可以

Y y;
tuple<char(&)[10], Y&>(a, y);

有可能提出无法构造的 tuple 类型。如果无法初始化的元素的索引低于需要初始化的元素,则会发生这种情况。例如:tuple<char[10], int&>

总而言之,tuple 构造在语义上只是一组单独的基本构造。

也可以使用 make_tuple(参见 std::make_pair)辅助函数构造 Tuples。这使得构造更方便,从而使程序员无需显式指定元素类型

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> 的 tuple。

有时不希望使用普通非引用类型,例如,如果元素类型无法复制。因此,程序员可以控制类型推导,并声明应使用对 const 的引用或对非 const 类型的引用作为元素类型。这是通过两个辅助模板函数完成的:boost::refboost::cref。可以使用这些函数包装任何参数以获得所需的类型。该机制不会损害 const 正确性,因为使用 ref 包装的 const 对象会导致具有 const 引用类型的 tuple 元素(请参阅下面的第五个示例)。例如

A a; B b; const A ca = a;
make_tuple(cref(a), b);      // creates tuple<const A&, B>
make_tuple(ref(a), b);       // creates tuple<A&, B>
make_tuple(ref(a), cref(b)); // creates tuple<A&, const B&>
make_tuple(cref(ca));        // creates tuple<const A&>
make_tuple(ref(ca));         // creates tuple<const A&>

默认情况下,make_tuple 函数的数组参数被推导为对 const 类型的引用;无需使用 cref 包装它们。例如

make_tuple("Donald", "Daisy");

这将创建一个类型为 tuple<const char (&)[7], const char (&)[6]> 的对象(请注意,字符串文字的类型是 const 字符数组,而不是 const char*)。但是,要让 make_tuple 创建具有非 const 数组类型元素的 tuple,必须使用 ref 包装器。

函数指针被推导为普通非引用类型,即普通函数指针。tuple 也可以保存对函数的引用,但是不能使用 make_tuple 构造这样的 tuple(将产生 const 限定的函数类型,这是非法的)

void f(int i);
  ...
make_tuple(&f); // tuple<void (*)(int)>
  ...
tuple<tuple<void (&)(int)> > a(f) // ok
make_tuple(f);                    // not ok

tuple 元素通过表达式访问

t.get<N>()

get<N>(t)

其中 t 是 tuple 对象,N 是一个常量整型表达式,指定要访问的元素的索引。根据 t 是否为 const,get 将第 N 个元素作为对 const 或非 const 类型的引用返回。第一个元素的索引为 0,因此 N 必须介于 0k-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>();        // ok
int j = get<0>(ct);                       // ok
get<0>(t) = 5;                            // ok
get<0>(ct) = 5;                           // error, can't assign to const
  ...
double e = get<1>(t); // ok
get<1>(t) = 3.14;     // ok
get<2>(t) = A();      // error, can't assign to const
A aa = get<3>(t);     // error: index out of bounds
  ...
++get<0>(t);  // ok, can be used as any variable

[注意: MS Visual C++ 编译器不支持成员 get 函数。此外,编译器在没有显式命名空间限定符的情况下很难找到非成员 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); // ok
a = t;                     // ok

在这两种情况下,执行的转换是

  • char -> int,
  • B* -> A*(派生类指针到基类指针),
  • B -> C(用户定义的转换),以及
  • D -> C(用户定义的转换)。

请注意,也定义了从 std::pair 类型赋值

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

Tuples 将运算符 ==!=<><=>= 简化为相应的基本运算符。这意味着,如果在两个 tuples 的所有元素之间定义了这些运算符中的任何一个,则也在 tuples 之间定义了相同的运算符。两个 tuples ab 的相等运算符定义为

  • a == b 当且仅当对于每个 iai == bi
  • a != b 当且仅当存在 iai != bi

运算符 <><=>= 实现字典顺序。

请注意,尝试比较两个长度不同的 tuples 会导致编译时错误。此外,比较运算符是“短路”的:基本比较从第一个元素开始,并且仅在结果明确之前执行。

例子

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, does not print "All the..."

Tiers 是 tuples,其中所有元素都是非 const 引用类型。它们通过调用 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)) 也可以实现相同的结果。

包含非 const 引用作为元素的 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 打印到标准输出流。像这样的 tuple 解包操作可以在 ML 和 Python 中找到。当调用返回 tuples 的函数时,它很方便。

绑定机制也适用于 std::pair 模板

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

还有一个名为 ignore 的对象,允许您忽略由 tuple 赋值的元素。这个想法是,函数可能会返回一个 tuple,而您只对其中的一部分感兴趣。例如(请注意,ignore 在 tuples 子命名空间下)

char c;
tie(tuples::ignore, c) = std::make_pair(1, 'a');

全局 operator<< 已针对 std::ostream 重载,以便通过为每个元素递归调用 operator<< 来输出 tuples。

类似地,全局 operator>> 已被重载,以通过为每个元素递归调用 operator>>std::istream 中提取 tuples。

元素之间的默认分隔符是空格,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(']') >> tuples::set_delimiter(':');
cin >> j;

将数据读取到 tuples ij 中。

请注意,通常情况下,提取具有 std::string 或 C 风格字符串元素的 tuples 不起作用,因为流式 tuple 表示可能无法明确解析。

所有 tuple 访问和构造函数都是小的内联单行代码。因此,与使用手写 tuple 类相比,一个不错的编译器可以消除使用 tuples 的任何额外成本。特别是,对于一个不错的编译器,以下代码之间没有性能差异

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 用法。

根据编译器的优化能力,与使用非 const 引用参数作为从函数返回多个值的机制相比,tier 机制可能会有轻微的性能损失。例如,假设以下函数 f1f2 具有等效的功能

void f1(int&, double&);
tuple<int, double> f2();

然后,在下面的代码中,调用 #1 可能比 #2 稍快

int i; double d;
  ...
f1(i,d);         // #1
tie(i,d) = f2(); // #2

有关效率的更深入讨论,请参阅 [1, 2]。

由于过多的模板实例化,编译 tuples 可能会很慢。根据编译器和 tuple 长度,编译 tuple 构造可能比编译等效的显式编写的类(例如上面的 hand_made_tuple 类)慢 10 倍以上。但是,由于实际程序除了 tuple 定义之外还可能包含大量代码,因此这种差异可能不明显。对于非常频繁地使用 tuples 的程序,测得的编译时间增加了 5% 到 10%。在相同的测试程序中,编译的内存消耗增加了 22% 到 27%。有关详细信息,请参阅 [1, 2]。

库代码是(?)标准 C++,因此该库适用于符合标准的编译器。以下是编译器列表以及每个编译器的已知问题

编译器

问题

gcc 2.95

-

edg 2.44

-

Borland 5.5

不能使用函数指针或成员指针作为 tuple 元素

Metrowerks 6.2

不能使用 refcref 包装器

MS Visual C++

没有引用元素(tie 仍然有效)。不能使用 refcref 包装器

高级功能(描述了一些元函数等)。

一些设计/实现决策背后的原理.

Gary Powell 一直是不可或缺的帮手。特别是,用于 tuples 的流操纵器是他的想法。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 机制的想法来自 Ian McCulloch 的一篇旧的 usenet 文章,他在文章中为 std::pair 提出了类似的东西。

[1] Järvi J.:C++ 中的 Tuples 和多重返回值,TUCS 技术报告第 249 号,1999 年。

[2] Järvi J.:标准 C++ 中的 ML 风格 Tuple 赋值 - 扩展多重返回值形式,TUCS 技术报告第 267 号,1999 年。

[3] Järvi J.:Tuple 类型和多重返回值,C/C++ 用户杂志,2001 年 8 月。


Next