现代c++模板元编程:遍历tuple (4)

首先我们有两个for_each_tuple2_impl,不过别紧张,因为模板形参不同,所以这是两个不同的模板(函数模板没有部分特化)。又因为变长参数的实参数量可以为0,为了实例化的时候不会产生歧义,只能让第二个for_each_tuple2_impl不接受任何额外的模板参数。

接着我们看到for_each_tuple2,它的作用很简单,通过参数上的std::tuple<Ts...>自动推导出tuple元素的所有类型,然后存放在Ts里。习惯上我们给变长参数包的名字是以s结尾的,象征参数包里可能有不止一个类型参数。

接下来才是重头戏。当我们这样调用for_each_tuple2_impl<std::tuple<Ts...>, Functor, Ts...>时,实际上会展开成for_each_tuple2_impl<std::tuple<Type1, Type2, ..., TypeN>, Functor, Type1, Type2, ..., TypeN.>。

对应到我们的template <typename Tuple, typename Functor, typename First, typename... Ts>, First就会是Type1,而其他剩下来的类型又会被收集到for_each_tuple2_impl的Ts里,这样我们就分离出了第一次个tuple里的元素。

然后我们使用std::get<First>(t)获取到这个元素,然后递归重复上述步骤。

Ts的第一个参数会被逐个分离,最后一直到Ts和First都为空,这是递归就该结束了,所以我们写出了第二个for_each_tuple2_impl模板来处理这一情况。

因为tuple的类型参数列表的顺序和其中包含元素是对应的,所以我们可以实现遍历。

到目前为止我们的for_each工作得很好,然而当我们传入了std::make_tuple(1,2,3),编译器又一次爆炸了。

好奇的你一定又在思考为什么了。不过这回你应该很快就有了头绪,tuple<int,int,int>,存在一样的类型参数,这时候std::get会不会不知道该获取的是哪个元素呢?

你猜对了,get的文档里是这么说的Fails to compile unless the tuple has exactly one element of that type.,意思是当某个类型A出现了不止一次时,使用get<A>会导致编译出错。

因此这个方案是有重大缺陷的,我们不能保证tuple里总是不同类型的数据。因此这条路走到死胡同里了。

折叠表达式——使用变长模板参数的正确解法

别气馁,尝试失败也是模板元编程的乐趣之一。更何况现代c++里有相当多的实用工具可以加以利用,比如integer_sequence和折叠表达式。

折叠表达式用于按照给定的模式展开变长模板参数包,而integer_sequence则可以用来包含0-N的整数类型非类型模板参数,在我上一篇介绍模板元编程的文章里有介绍,这里不再赘述。

使用integer_sequence可以构建一个包含所有tuple元素索引的编译期整数常量序列,配合折叠表达式可以把这些索引展开利用,这样正好可以让get用上每一个索引:

template <typename Tuple, typename Functor, std::size_t... Is> constexpr void for_each_tuple3_impl(const Tuple &t, Functor &&f, std::index_sequence<Is...>) { // 展开成(f(std::get<0>(t)),f(std::get<1>(t)),...) (f(std::get<Is>(t)), ...); } template <typename Tuple, typename Functor> constexpr void for_each_tuple3(const Tuple &t, Functor &&f) { // std::make_index_sequence<std::tuple_size_v<Tuple>>产生一个index_sequence<0,1,2,..,N> for_each_tuple3_impl(t, std::forward<Functor>(f), std::make_index_sequence<std::tuple_size_v<Tuple>>()); }

这次不再是模板递归了,我们生成了所有元素的索引,然后教编译器硬编码了所有的get操作,形式上不太像但确确实实完成了遍历操作。

当然老问题是少不了要问的,tuple为空的时候这个方案能正常工作吗?

答案是肯定的,标准规定了std::make_index_sequence<0>会生成一个空的序列,而逗号运算符的一元折叠表达式对于空的参数包会安全地返回void,所以在传入一个空tuple时我们的函数是noop的。

这种方案简单粗暴,同时也是三种方法中最直观的。

而且这个方案不会产生一大堆的模板实例,生成的二进制文件也是清爽干净的。同时因为不是递归,也不会受到递归深度限制的影响。

这就是现代c++在模板元编程上的威力。

总结

我们一共实现了三种遍历tuple的方法,从原始到现代,从复杂到简单。

同时我们还踩掉了一些坑,在今后的开发中只要留意类似的问题也能及时避免了。

当然,我写的方案仍有很大的提升空间,你可以自己进行尝试改进。

不过我最想说的还是现代c++真的极大简化了模板元编程,把模板元编程从一个复杂抽象的黑魔法变成了直观易于理解的开发技巧,应该有更多的人来体验使用现代c++的乐趣,c++已经脱胎换骨了。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsfsgd.html