tuple是c++11新增的数据结构,通过tuple我们可以方便地把各种不同类型的数据组合在一起。有了这样的数据结构我们就可以轻松模拟多值返回等技巧了。
tuple和其他的容器不同,标准库没有提供适用于tuple的迭代器,也没有提供tuple类型的迭代接口。所以当我们想要遍历tuple的时候只能自己动手了。
所以这篇文章我们会实现一个简单的接口用来遍历各种tuple,顺便一窥现代c++中的模板元编程。
本文索引接口设计
为什么要遍历tuple呢?通常我们确实不需要逐个遍历tuple的数据,通过使用get取出特定位置的元素就满足大部分的应用需求了。
但偶尔我们也会想要把某一个泛型算法应用到tuple的每一项成员上,虽然不多见但也确实有需求的场景存在。因此如何实现对tuple的遍历就被摆上了议程。
然而遗憾的是get需要的索引只能是编译期常量,这导致我们无法依赖现有的循环语句去实现索引的递增,因此只有两条路可供选择:硬编码每一项索引和模板元编程。我是个不喜欢硬编码的人,所以我选择了后者。
把STL里的通用容器算法实现一遍工程量太大了,而且很明显一篇文章也讲不完。我决定实现标准库里的for_each,正好也契合今天的主题——遍历tuple。
标准库的for_each是这样的template <class Iterator, class UnaryFunction> void for_each(Iterator first, Iterator last, UnaryFunction f),其中UnaryFunction的函数签名是void fun(const Type &a)。
通过for_each我们可以顺序遍历容器中的每一项数据,我们的for_each_tuple也将实现类似的功能——顺序遍历tuple的每一项元素。
不过前面已经提到了,tuple是没有迭代器的,因此我们的函数得改个样子:template <class Tuple, class Functor> void for_each_tuple(const Tuple &, Functor &&)。因为不能使用迭代器,所以我们传了tuple的引用进函数。
当然,c++17里tuple是constexpr类型,所以你还可以给我们的for_each加上constexpr。
函数内部要做的事其实也很简单,就是对每一项元素调用f即可,在这里我们不考虑其他一些细节,我们的接口形式上应该是这样子的(伪代码):
template <class Tuple, class Functor> constexpr void for_each_tuple(const Tuple &t, Functor &&f) { for element in t { f(t); } } 实现接口接口设计好了,下面我们就该实现for element in t的部分了。
接下来我会介绍三种实现遍历tuple的方法,以及一种存在缺陷的方法,首先我们从最原始的方案开始。
初步尝试距离c++11发布已经快整整十年了,想必大家也习惯于书写c++11的代码了。不过让我们把时间倒流回c++11前的时代,那时候既没有constexpr,也没有变长模板参数,相当的原始而蛮荒。
那么问题来了,那时候有tuple吗?当然有,boost里的tuple的历史在c++11前就已经开始了。
其中的秘诀就在于模板递归,这是一种经典的元编程手段,解铃还须系铃人,我们的foreach也需要借助这种技术。
现在我们来看一下不使用编译期计算和变长模板参数的原始方案:
template <typename Tuple, typename Functor, int Index> void for_each_tuple_impl(Tuple &&t, Functor &&f) { if (Index >= std::tuple_size<std::remove_reference_t<Tuple>>::value) { return; } else { f(std::get<Index>(t)); for_each_tuple_impl<Tuple, Functor, Index+1>(std::forward<Tuple>(t), std::forward<Functor>(f)); } } template <typename Tuple, typename Functor> void for_each_tuple(Tuple &&t, Functor &&f) { for_each_tuple_impl<Tuple, Functor, 0>(std::forward<Tuple>(t), std::forward<Functor>(f)); }我们用std::remove_reference_t来把Tuple从引用类型转化为被引用的tuple的类型,原因是模板函数的右值引用参数会自动根据引用折叠的规则转换为左值引用或者右值引用,而我们不能从引用类型调用std::tuple_size获取tuple的长度。
整体的思路其实很简单,我们从0开始,每遍历处理完一项就让index+1,然后递归调用impl。如果了最后一个元素+1的位置,函数就返回。这样遍历就结束了。
注意f上的std::forward,我们用右值引用的目的是接受包括lambda在内的所有可调用对象,这些对象可以是一个lambda字面量,可以是一个具名的存储了lambda的变量,还以可以是函数指针或者任何重载了template <typename T> void operator()(const T&)运算符的类的实例。所以我们很难假设这么广范围内的可调用对象都是可以被复制的,所以保险起见我们使用了模板的右值引参数来将不可以复制的内容用右值引用捕获。当然因为移动语义会产生副作用,这点用户得自己负担,而我们也不得不对f使用std::forward进行完美转发。不过这样好处也不是没有,至少我们省去了很多不必要的复制。