用法

优质
小牛编辑
121浏览
2023-12-01

Tuples 位于名字空间 tuples, 后者又位于名字空间 boost. 使用这个库要包含头文件 "boost/tuple/tuple.hpp"。关系操作符的定义在头文件 "boost/tuple/tuple_comparison.hpp"中。tuples 的输入输出定义在头文件 "boost/tuple/tuple_io.hpp"中。tuple 的一些关键部件(tiemake_tuple)也可以直接在名字空间 boost 中使用。在本节中,我们将讨论如何在一些常见情形下使用 tuples ,以及如何可以扩展这个库的功能以最好地符合我们的意图。我们将从构造 tuples 开始,并逐渐转移到其它主题,包括如何利用 tuples 的细节。

构造 Tuples

构造一个 tuple 包括声明各种类型,并可选地提供一组兼容类型的初始值。[1]

[1] 在特化时 tuple ,构造函数的参数不必与元素的类型精确相同,只要它们可以隐式地转换为元素的类型就可以了。

boost::tuple<int,double,std::string> 
  triple(42,3.14,"My first tuple!");

类模板 tuple 模板参数指定了元素的类型。前面这个例子示范了一个带有三个类型的 tuple 的创建:一个 int, 一个 double, 和一个 std::string. 并向构造函数提供了三个参数来初始化所有三个元素的值。也可以传递少于元素数量的参数,那样的话剩下的元素将被缺省初始化。

boost::tuple<short,int,long> another;

在这个例子中,another 有类型为 short, int, 和 long 的元素,并且它们都被初始化为0.[2] 不管你的 tuple 是什么类型,这就是它如何定义和构造的方式。所以,如果你的 tuple 有一个元素类型不能缺省构造,你就需要自己初始化它。与定义 struct 相比,tuple 更容易声明、定义和使用。还有一个便于使用的函数,make_tuple,它使得创建 tuples 更加容易。它自动推断元素的类型,不用你来重复指定(这也会是出错的机会!)。

[2] 在一个模板上下文中,T() 对于一个内建类型而言意味着初始化为零。

boost::tuples::tuple<int,double> get_values() {
  return boost::make_tuple(6,12.0);
}

函数 make_tuple 类似于 std::make_pair. 缺省情况下,make_tuple 设置元素类型为非const, 非引用的,即是最简单的、根本的参数类型。例如,考虑以下变量:

int plain=42;
int& ref=plain;
const int& cref=ref;

这三个变量根据它们的cv限定符(常量性)以及是否引用来命名。通过调用以下 make_tuple 创建的 tuple 都带有一个 int 元素。

boost::make_tuple(plain);
boost::make_tuple(ref);
boost::make_tuple(cref);

这种行为不总是正确的,但通常是,这正是为什么它是缺省行为的原因。为了使一个 tuple 的元素设为引用类型,你要使用函数 boost::ref, 它来自另一个名为 Boost.Ref 的 Boost 库。以下三行代码使用了我们前面定义的三个变量,但这次 tuple 带有一个 int& 元素,除了最后一个,它带的是一个 const int& 元素 (我们不能去掉 cref 的常量性):

boost::make_tuple(boost::ref(plain));
boost::make_tuple(boost::ref(ref));
boost::make_tuple(boost::ref(cref));

如果元素需要是 const 引用的,就使用来自 Boost.Ref 的 boost::cref。下面三个 tuples 带有一个 const int& 元素:

boost::make_tuple(boost::cref(plain));
boost::make_tuple(boost::cref(ref));
boost::make_tuple(boost::cref(cref));

refcref 在其它地方也经常使用。事实上,它们原先是作为 Boost.Tuple 库的一部分而建立的,但后来因为它们的通用性而移出去成为一个独立的库。

访问 tuple 元素

一个 tuple 的元素可以通过 tuple 成员函数 get 或普通函数 get 来访问。它们都要求用一个常量整型表达式来指定要取出的元素的索引。

#include <iostream>
#include <string>

#include "boost/tuple/tuple.hpp"

int main() {
  boost::tuple<int,double,std::string> 
    triple(42,3.14,"The amazing tuple!"); 

  int i=boost::tuples::get<0>(triple);
  double d=triple.get<1>();
  std::string s=boost::get<2>(triple);
}

这个例子中,一个三元素的 tuple 取名为 tripletriple 含有一个 int, 一个 double, 和一个 string, 它们可以用 get 函数取出。

int i=boost::tuples::get<0>(triple);

这里,你看到的是普通函数 get 。它把 tuple 作为一个参数。注意,给出一个无效的索引会导致一个编译期错误。这个函数的前提是索引值对于给定的 tuple 类型必须有效。

double d=triple.get<1>();

这段代码使用的是成员函数 get. 它也可以写成这样:

double& d=triple.get<1>();

这个绑定到一个引用的方式可以使用,因为 get 总是返回一个到元素的引用。如果 tuple, 或者其类型,是 const 的, 则返回一个 const 引用。这两个函数是等价的,但在某些编译器上,只有普通函数可以正确工作。普通函数有一个优点,它提供了与 tuple 之外的其它类型一致的提取元素的风格。通过索引来访问 tuple 的元素而不是通过名字来访问,这样做的一个优点是它可以支持泛型的解决方法,因为这样做不依赖于某个特定的名字,仅仅是一个索引值。稍后对此有更多介绍。

Tuple 赋值及复制构造

tuples 可以被赋值和被复制构造,可以在两个 tuple 间进行,只要它们的元素类型可以相互转换。要赋值或复制 tuples, 就是执行成员间的赋值或复制,因此这两个 tuples 必须具有相同数量的元素。源 tuple 的元素必须可以转换为目标 tuple 的元素。以下例子示范了如何使用。

#include <iostream>
#include <string>

#include "boost/tuple/tuple.hpp"

class base {
public:
  virtual ~base() {};
  virtual void test() {
    std::cout << "base::test()\n";
  }
};

class derived : public base {
public:
  virtual void test() {
    std::cout << "derived::test()\n";
  }
};

int main() {
  boost::tuple<int,std::string,derived> tup1(-5,"Tuples");
  boost::tuple<unsigned int,std::string,base> tup2;
  tup2=tup1;

  tup2.get<2>().test();
  std::cout << "Interesting value: " 
    << tup2.get<0>() << '\n';

  const boost::tuple<double,std::string,base> tup3(tup2);
  tup3.get<0>()=3.14;
}

这个例子开始时定义两个类,basederived, 它们被用作两个 tuple 类型的元素。第一个 tuple 有三个元素,类型为 int, std::string, 和 derived. 第二个 tuple 有三个兼容类型的元素,分别为 int, std::string, 和 base. 因此,这两个 tuples 符合赋值的要求,这就是为什么 tup2=tup1 有效的原因。在这个赋值中,tup1 的第三个元素类型为 derived, 被赋值给 tup2 的第三个元素,后者类型为 base. 赋值可以成功,但 derived 对象被切割,因此这样会破坏多态性。

tup2.get<2>().test();

这一行取出一个 base&, 但 tup2 中的对象类型为 base, 因此它将调用 base::test. 我们可以通过把 tuples 修改为分别包含 basederived 的引用或指针来获得多态行为。注意数值转换的危害(精度损失、正负溢出)也会出现在 tuples 间的转换。这种危险的转换可以通过 Boost.Conversion 库的帮助来变得安全,请参见"Library 2: Conversion"。

下一行是复制构造一个新的 tuple, tup3, 它的类型不同但还是兼容于 tup2.

const boost::tuple<double,std::string,base> tup3(tup2);

注意,tup3 被声明为 const. 这意味着本例中有一个错误。看你能否找到它。我等一下你...,你看出来了吗?就是这里:

tup3.get<0>()=3.14;

因为 tup3const, get 将返回一个 const double&. 这意味着这个赋值语句是非法的,这个例子不能编译。tuples 间的赋值与复制构造是符合直觉的,因为它的语义与单个元素是相同的。通过下面这个例子,我们来看看如何在 tuples 间的派生类至基类的赋值中获得多态行为。

derived d;
boost::tuple<int,std::string,derived*> 
  tup4(-5,"Tuples",&d);
boost::tuple<unsigned int,std::string,base*> tup5;
tup5=tup4;

tup5.get<2>()->test();

boost::tuple<int,std::string,derived&>
  tup6(12,"Example",d); 

boost::tuple<unsigned int,std::string,base&> tup7(tup6);

tup7.get<2>()->test();

在这两种情况下,都会调用 derived::test ,这正是我们想要的。tup6tup7 间不能赋值,因为你不能对一个引用进行赋值,这就是为什么 tup7 要从 tup6 复制构造,以及 tup6 要用 d 进行初始化的原因。因为 tup4tup5 对它们的第三个元素使用的是指针,因此它们可以支持赋值。注意,通常在 tuple 中最好使用智能指针(与裸指针相比),因为它们可以减轻对指针所指向的资源的生存期管理的压力。但是,正如 tup4tup5 所示,在 tuples 中,指针不总是指向需要进行内存管理的东西的。(参考 "Library 1: Smart_ptr 1",可获得 Boost 强大的智能指针的更多信息)

比较 Tuples

要比较 tuples, 你必须包含头文件 "boost/tuple/tuple_comparison.hpp". tuple 的关系操作符有 ==,!=,&lt;,&gt;,&lt;=&gt;=, 它们按顺序地对要比较的 tuples 中的每一对元素调用相应的操作符。这些比较是短路的,即只比较到可以得到正确结果为止。只有具有相同数量元素的 tuples 可以进行比较,并且显然两个 tuples 的对应的元素必须是可比较的。如果两个 tuples 的所有元素对都相等,则相等比较返回 true 。如果任意一对元素的相等比较返回 false, operator== 也将返回 false。不等比较也是类似的,但返回的是相反的结果。其它关系操作符按字典序进行比较。

以下例程示范比较操作符的行为。

#include <iostream>
#include <string>

#include "boost/tuple/tuple.hpp"
#include "boost/tuple/tuple_comparison.hpp"

int main() {
  boost::tuple<int,std::string> tup1(11,"Match?");
  boost::tuple<short,std::string> tup2(12,"Match?");

  std::cout << std::boolalpha;

  std::cout << "Comparison: tup1 is less than tup2\n";

  std::cout << "tup1==tup2: " << (tup1==tup2) << '\n'; 
  std::cout << "tup1!=tup2: " << (tup1!=tup2) << '\n';
  std::cout << "tup1<tup2: " << (tup1<tup2) << '\n';
  std::cout << "tup1>tup2: " << (tup1>tup2) << '\n';
  std::cout << "tup1<=tup2: " << (tup1<=tup2) << '\n';
  std::cout << "tup1>=tup2: " << (tup1>=tup2) << '\n';

  tup2.get<0>()=boost::get<0>(tup1); //tup2=tup1 also works

  std::cout << "\nComparison: tup1 equals tup2\n"; 

  std::cout << "tup1==tup2: " << (tup1==tup2) << '\n'; 
  std::cout << "tup1!=tup2: " << (tup1!=tup2) << '\n';
  std::cout << "tup1<tup2: " << (tup1<tup2) << '\n';
  std::cout << "tup1>tup2: " << (tup1>tup2) << '\n';
  std::cout << "tup1<=tup2: " << (tup1<=tup2) << '\n';
  std::cout << "tup1>=tup2: " << (tup1>=tup2) << '\n';
}

如你所见,这两个 tuples, tup1tup2, 并不是严格相同的类型,但它们的类型是可比较的。在第一组比较中,tuples 的第一个元素值不同,而在第二组中,tuples 是相同的。以下是程序的运行输出。

Comparison: tup1 is less than tup2
tup1==tup2: false
tup1!=tup2: true
tup1<tup2: true
tup1>tup2: false
tup1<=tup2: true
tup1>=tup2: false

Comparison: tup1 equals tup2
tup1==tup2: true
tup1!=tup2: false
tup1<tup2: false
tup1>tup2: false
tup1<=tup2: true
tup1>=tup2: true

支持比较的一个重要方面是,tuples 可以被排序,这意味着它们可以在关联容器中被排序。有些时候,我们需要按 tuple 中的某一个元素进行排序(建立一个弱序),我们可以用一个简单的泛型方法来实现。

template <int Index> class element_less {
public:
  template <typename Tuple> 
  bool operator()(const Tuple& lhs,const Tuple& rhs) const {
    return boost::get<Index>(lhs)<boost::get<Index>(rhs); 
  } 
};

这显示了使用索引而不是用名字来访问元素的优势;它可以很容易地创建泛型的构造来执行强大的操作。我们的 element_less 可以这样用:

#include <iostream>
#include <vector> 
#include "boost/tuple/tuple.hpp"
#include "boost/tuple/tuple_comparison.hpp"

template <int Index> class element_less {
public:
  template <typename Tuple> 
  bool operator()(const Tuple& lhs,const Tuple& rhs) const {
    return boost::get<Index>(lhs)<boost::get<Index>(rhs); 
  } 
};

int main() {
  typedef boost::tuple<short,int,long,float,double,long double> 
    num_tuple;

  std::vector<num_tuple> vec;

  vec.push_back(num_tuple(6,2));
  vec.push_back(num_tuple(7,1));
  vec.push_back(num_tuple(5));

  std::sort(vec.begin(),vec.end(),element_less<1>());

  std::cout << "After sorting: " << 
    vec[0].get<0>() << '\n' <<
    vec[1].get<0>() << '\n' <<
    vec[2].get<0>() << '\n';
}

vec 由三个元素组成。使用从我们前面创建的模板所特化的 element_less&lt;1&gt; 函数对象,来执行基于 tuples 的第二个元素的排序。这类函数对象还有更多的应用,如用于查找指定的 tuple 元素。

绑定 Tuple 元素到变量

Boost.Tuple 库的一个方便的特性是"绑定" tuples 到变量。绑定者就是用重载函数模板 boost::tie 所创建的 tuples,它的所有元素都是非const 引用类型。因此,ties 必须使用左值进行初始化,从而 tie 的参数也必须是非 const 引用类型。由于结果 tuples 具有非 const 引用类型,对这样一个 tuple 的元素进行赋值,就会通过非 const 引用赋值给调用 tie 时的左值。这样就绑定了一个已有变量给 tuple, tie 的名字由此而来!

以下例子首先示范了一个通过返回 tuple 获得值的明显的方法。然后,它示范了通过一个 tied tuple 直接赋值给变量以完成相同操作的方法。为了让这个例子更为有趣,我们开始时定义了一个返回两个数值的最大公约数和最小公倍数的函数。当然,这两个结果值被组合成一个 tuple 返回类型。你将发现计算最大公约数和最小公倍数的函数来自于另一个 Boost 库——Boost.Math.

#include <iostream>
#include "boost/tuple/tuple.hpp"
#include "boost/math/common_factor.hpp"

boost::tuple<int,int> gcd_lcm(int val1,int val2) {
  return boost::make_tuple(
    boost::math::gcd(val1,val2),
    boost::math::lcm(val1,val2));
}

int main() {
  //"老"方法
  boost::tuple<int,int> tup;
  tup=gcd_lcm(12,18);
  int gcd=tup.get<0>();  // 译注:原文为 int gcd=tup.get<0>()); 明显有误
  int lcm=tup.get<1>();  // 译注:原文为 int gcd=tup.get<1>()); 明显有误

  std::cout << "Greatest common divisor: " << gcd << '\n';
  std::cout << "Least common multiple: " << lcm << '\n';

  //"新"方法
  boost::tie(gcd,lcm)=gcd_lcm(15,20);

  std::cout << "Greatest common divisor: " << gcd << '\n';
  std::cout << "Least common multiple: " << lcm << '\n';
}

有时我们并不是对返回的 tuple 中所有的元素感兴趣,tie 也可以支持这种情况。有一个特殊的对象,boost:: tuples::ignore,它忽略一个 tuple 元素的值。如果前例中我们只对最大公约数感兴趣,我们可以这样写:

boost::tie(gcd,boost::tuples::ignore)=gcd_lcm(15,20);

另一种方法是创建一个变量,传递给 tie, 然后在后面的处理中忽略它。这样做会令维护人员弄不清楚这个变量为什么存在。使用 ignore 可以清楚地表明代码将不使用 tuple 的那个值。

注意,tie 也支持 std::pair. 用法与从 boost::tuples 绑定值一样。

std::pair<short,double> p(3,0.141592);
short s;
double d;

boost::tie(s,d)=p;

绑定 tuples 不仅仅是方便使用;它有助于使代码更为清晰。

Tuples的流操作

在本章的每一个例子中,取出 tuples 的元素都只是为了能够把它们输出到 std::cout. 可以象前面那样做,但还有更容易的方法。tuple 库支持输入和输出流操作;tuple 重载了operator&gt;&gt;operator&lt;&lt;。还有一些操纵器用于改变输入输出的缺省分隔符。对输入操作改变分隔符改变 operator&gt;&gt; 查找元素值的结果。我们用一个简单的读写 tuples 的程序来测试一下这些情况。注意,要使用 tuple 的流操作,你必须包含头文件 "boost/tuple/tuple_io.hpp".

#include <iostream>
#include "boost/tuple/tuple.hpp"
#include "boost/tuple/tuple_io.hpp"

int main() {
  boost::tuple<int,double> tup1;
  boost::tuple<long,long,long> tup2;

  std::cout << "Enter an int and a double as (1 2.3):\n";
  std::cin >> tup1;

  std::cout << "Enter three ints as |1.2.3|:\n";
  std::cin >> boost::tuples::set_open('|') >>
  boost::tuples::set_close('|') >>
  boost::tuples::set_delimiter('.') >> tup2;

  std::cout << "Here they are:\n"
    << tup1 << '\n'
    << boost::tuples::set_open('\"') <<
  boost::tuples::set_close('\"') <<
  boost::tuples::set_delimiter('-');

  std::cout << tup2 << '\n';
}

上面这个例子示范了如何对 tuples 使用流操作符。tuples 的缺省分隔符是:( (左括号) 作为开始分隔符,) (右括号) 作为结束分隔符,空格用于分隔各个 tuple 元素值。这意味着我们的程序要正确运行的话,我们需要象这样输入:(12 54.1)|4.5.3|. 以下是运行的例子。

Enter an int and a double as (1 2.3):
(12 54.1)
Enter three ints as |1.2.3|:
|4.5.3|
Here they are:
(12 54.1)
"4-5-3"

对流操作的支持是很方便的,通过对分隔符操纵器的支持,可以很容易让使用 tuple 的代码的流操作兼容于已有代码。

关于 Tuples 的更多

还有很多我们没有看到的工具可用于 tuples。这些更为先进的特性对于创建使用 tuple 的泛型结构至为重要。例如,你可以获得一个 tuple 的长度(即元素的数量),取出某个元素的类型,使用 null_type tuple 哨兵来终结递归模板实例化。

不可能用一个 for 循环来迭代一个 tuple 里的元素,因为 get 要求提供一个常量整型表达式。但是,使用模板元编程,我们可以打印一个 tuple 的所有元素。

#include <iostream>
#include <string>
#include "boost/tuple/tuple.hpp"

template <typename Tuple,int Index> struct print_helper {
  static void print(const Tuple& t) {
    std::cout << boost::tuples::get<Index>(t) << '\n';
    print_helper<Tuple,Index-1>::print(t);
  }
};

template<typename Tuple> struct print_helper<Tuple,0> {
  static void print(const Tuple& t) {
    std::cout << boost::tuples::get<0>(t) << '\n';
  }
};

template <typename Tuple> void print_all(const Tuple& t) {
  print_helper<
    Tuple,boost::tuples::length<Tuple>::value-1>::print(t);
}

int main() {
  boost::tuple<int,std::string,double> 
    tup(42,"A four and a two",42.424242);

  print_all(tup);
}

在这个例子中有一个辅助类模板,print_helper, 它是一个元程序,访问 tuple 的所有索引,并对每个索引打印出相应元素。偏特化版本用于结束模板递归。函数 print_all 用它的 tuple 参数的长度以及这个 tuple 来调用 print_helper 构造函数。tuple 的长度可以这样来取得:

boost::tuples::length<Tuple>::value

这是一个常量整型表达式,这意味着它可以作为第二个模板参数传递给 print_helper. 但是,我们的解决方案中有一个警告,我们看看这个程序的输出结果就清楚了。

42.4242
A four and a two
42

我们按反序来打印元素了!虽然有些情况下可能会需要这种用法(他狡猾地辩解说),但在这里不是。问题在于 print_helper 先打印 boost::tuples::length&lt;Tuple&gt;::value-1 元素的值,然后才到前一个元素,直到偏特化版本打印第一个元素值为止。我们不应该使用第一个元素作为特化条件以及从最后一个元素开始,而是需要从第一个元素开始以及使用最后一个元素作为特化条件。这怎么可能?在你明白到 tuple 是以一个特殊的类型 boost::tuples:: null_type 作为结束以后,解决的方法就很明显了。我们可以确保一个 tuple 中的最后一个类型是 null_type, 这也意味着我们的解决方法应该是对 null_type 进行特化或函数重载。

剩下的问题就是取出第一个元素的值,然后继续,并在列表尾部结束。tuples 提供了成员函数 get_headget_tail 来访问其中的元素。顾名思义,get_head 返回数值序列的头,即第一个元素的值。get_tail 返回一个由该 tuple 中除了第一个值以外的其它值组成的 tuple. 这就引出了 如下 print_all 的解决方案。

void print_all(const boost::tuples::null_type&) {}

template <typename Tuple> void print_all(const Tuple& t) {
  std::cout << t.get_head() << '\n';
  print_all(t.get_tail());
}

这个解决方案比原先的更短,并且按正确的顺序打印元素的数值。每一次函数模板 print_all 执行时,它打印 tuple 的第一个元素,然后用一个由 t 中除第一个值以外的其它值所组成的 tuple 递归调用它自己。当 tuple 没有数值时,结尾是一个 null_type, 将调用重载函数 print_all ,递归结束。

可以知道某个元素的类型有时是有用的,例如当你要在泛型代码中声明一个由 tuple 的元素初始化的变量时。考虑一个返回 tuple 中前两个元素的和的函数,它有一个额外的要求,即返回值的类型必须是两个中较大的那个类型(例如,考虑整数类型)。如果不清楚元素的类型,就不可能创建一个通用的解决方案。这正是辅助模板 element&lt;N,Tuple&gt;::type 要做的,如下例所示。我们所面对的问题不仅仅是计算哪个元素具有较大的类型,还有如何声明这个函数的返回值类型。这有点复杂,但我们可以通过增加一个间接层来解决。这个间接层以一个额外的辅助模板形式出现,它有一个责任:提供一个 typedef 以定义两种类型中的较大者。代码可能看起来有点多,但它的确可以完成任务。

#include <iostream>
#include "boost/tuple/tuple.hpp"
#include <cassert>
#include <typeinfo>  //译注:原文没有这行,不能通过编译

template <bool B,typename Tuple> struct largest_type_helper {
  typedef typename boost::tuples::element<1,Tuple>::type type;
};

template<typename Tuple> struct largest_type_helper<true,Tuple> {
  typedef typename boost::tuples::element<0,Tuple>::type type;
};

template<typename Tuple> struct largest_type {
  typedef typename largest_type_helper<
    (sizeof(boost::tuples::element<0,Tuple>)>
    sizeof(boost::tuples::element<1,Tuple>)),Tuple>::type type; 
};

template <typename Tuple> 
typename largest_type<Tuple>::type sum(const Tuple& t) {
  typename largest_type<Tuple>::type
    result=boost::tuples::get<0>(t)+
      boost::tuples::get<1>(t);

  return result;
}

int main() {
  typedef boost::tuple<short,int,long> my_tuple;

  boost::tuples::element<0,my_tuple>::type first=14;
  assert(typeid(first) == typeid(short));  
  //译注:原文为assert(type_id(first) == typeid(short)); 明显有误
  boost::tuples::element<1,my_tuple>::type second=27;
  assert(typeid(second) == typeid(int));
  //译注:原文为assert(type_id(second) == typeid(int)); 明显有误
  boost::tuples::element<
    boost::tuples::length<my_tuple>::value-1,my_tuple>::type 
    last;

  my_tuple t(first,second,last);

  std::cout << "Type is int? " << 
    (typeid(int)==typeid(largest_type<my_tuple>::type)) << '\n';

  int s=sum(t);
}

如果你不太清楚模板元编程的运用,不用担心,对于使用 Tuple 库这不是必需的。虽然这类代码有时会用到,它的思想其实也很简单。largest_type 从两个辅助类模板 largest_type_helper 中的一个获得 typedef ,具体使用哪一个就要靠那个布尔参数来特化了。这个参数通过比较 tuple (第二个模板参数) 的头两个元素的大小来决定。这样的结果是,typedef 会表现为两个类型中较大的那一个。我们的函数 sum 使用这个类型来作为返回值的类型,剩下的部分就是简单地对两个元素进行相加了。

这个例子的剩余部分示范了如何使用函数 sum, 还有如何从某个 tuple 元素的类型来声明变量。头两个对 tuple 中的索引使用了硬编码。

boost::tuples::element<0,my_tuple>::type first=14;
boost::tuples::element<1,my_tuple>::type second=27;

最后一个声明取出 tuple 的最后一个元素的索引,并用它作为辅助模板的输入来(通用地)声明这个类型。

boost::tuples::element<
  boost::tuples::length<my_tuple>::value-1,my_tuple>::type last;

Tuples 与 for_each

我们前面用于创建 print_all 函数的方法可以被推广至创建象 std::for_each 那 样的更通用的机制。例如,如果我们不想打印元素,而是想对它们取和或是复制它们,又或者我们只想打印它们中的一部分,要怎么做呢?对 tuple 的元素进行顺序访问并不简单,正如我们在前面的例子所见的一样。创建一个通用性的解决方案来接受一个函数或函数对象参数来调用 tuple 的元素是很有意义的。这样就不仅可以实现(有点限制的) print_all 函数的功能,还可以执行任何函数,只要这个函数可以接受 tuple 中的元素的类型。下面的例子创建了一个名为 for_each_element 的函数模板来实现这个功能。这个例子用两个函数对象作为参数来示范如何使用 for_each_element

#include <iostream>
#include <string>
#include <functional>
#include "boost/tuple/tuple.hpp"

template <typename Function> void for_each_element(
  const boost::tuples::null_type&, Function) {}

template <typename Tuple, typename Function> void 
for_each_element(Tuple& t, Function func) {
  func(t.get_head());
  for_each_element(t.get_tail(),func);
}

struct print {
  template <typename T> void operator()(const T& t) {
    std::cout << t << '\n';
  }
};

template <typename T> struct print_type {
  void operator()(const T& t) {
    std::cout << t << '\n';
  }

  template <typename U> void operator()(const U& u) {}
};

int main() {
  typedef boost::tuple<short,int,long> my_tuple;

  boost::tuple<int,short,double> nums(1,2,3.01);

  for_each_element(nums, print());
  for_each_element(nums, print_type<double>());
}

函数 for_each_element 重用了前面例子中的策略,通过重载函数的一个版本来接受类型为 null_type 的参数,表示已来到 tuple 元素的结尾,从而不做任何事。让我们来看看完成实际工作的那个函数。

template <typename Tuple, typename Function> void 
for_each_element(Tuple& t, Function func) {
  func(t.get_head());
  for_each_element(t.get_tail(),func);
}

第二个模板的函数参数指定了以 tuple 元素为参数进行调用的函数(或函数对象)。for_each_element 首先用 get_head 返回的元素来调用这个函数(对象)。要注意的是,get_head 返回的是 tuple 的当前元素。然后,它递归地用 tuple 的剩余元素来调用自已本身。第二次调用同样取出头一个元素并用它调用给定的函数(对象),然后再次递归,一直下去。最后,get_tail 发现没有元素了,就返回一个 null_type 实例,它匹配了那个非递归的 for_each_element 重载版本,从而结束了递归。这就是 for_each_element 的全部!

接下来,这个例子给出了两个示例函数对象,它们所用的技术可以在其它情况下重用。一个是 print 函数对象。

struct print {
  template <typename T> void operator()(const T& t) {
    std::cout << t << '\n';
  }
};

这个 print 函数对象没什么奇怪的地方,但正如它所做的那样,许多程序员不知道调用操作符可以是模板的!通常,函数对象可以对一个或多个类型进行特化,但对于 tuples 这样不行,因为它的元素可以是完全不同的类型。因此,不是对于函数对象本身进行特化,而是对调用操作符进行特化,这样做的另一个好处是它更简单,如下。

for_each_element(nums, print());

不需要给出类型,而如果使用特化函数对象则需要。把模板参数推给成员函数有些时候是很有用的,而且通常用户也更容易使用。

第二个函数对象打印指定类型的所有元素。这种过滤方法也可以用于取出相容类型的元素。

template <typename T> struct print_type {
  void operator()(const T& t) {
    std::cout << t << '\n';
  }

  template <typename U> void operator()(const U& u) {}
};

这个函数对象显示了另一个有用的技术,我称之为忽略重载(discarding overload)。它用于忽略传给它的除了 T 类型以外的所有元素。窍门就是除了指定类型外的其它类型的重载匹配。这可能会让你想到这种技术与另一个有密切的联系,即 sizeof 窍门和省略号(...)结构,后者被也用于在编译期进行决议,但它在这里不能使用,在这里,函数确实被调用了,只是没有做任何事情而已。这个函数对象可以这样用:

for_each_element(print_type<double>(),nums);

很容易用,也容易写,并且它有更多的价值。实际上 Tuple 库使用的函数对象可能没有这么多特性,它们也可以用于这种技术或其它的用法。