模板递归的另一个缺点是递归的最大深度有限制,在g++10.2上这个限制是900,也就是说超过900个元素的tuple我们是无法处理的,除非用编译器的命令行选项更改这一限制。不过通常也没人会写出有900多个元素的tuple。
还有一个需要考虑的情况,当我们传递了一个空的tuple进去会发生什么呢?
classic_for_each_tuple(std::tuple<>{}, [](const auto &element) { /* work */ });我们会得到一个编译错误,而我们所期望的是foreach什么也不做。问题发生在std::tuple_size_v<Tuple> - 1,当tuple为空时size为0,而对无符号数的0减去1会导致回环,从而导致get使用的索引的范围十分巨大,超过了模板递归深度限制;而更致命的是get一个无效的索引(tuple为空,任何索引都无效)是被static_assert断言的编译期错误,并且往往会产生一大长串错误信息导致debug困难。
不过别担心,这是个小问题,解决起来也不麻烦,还记得我们的模板元编程技巧吗?用重载或特化表示边界条件:
template <typename Functor> constexpr void classic_for_each_tuple(const std::tuple<> &, Functor &&) { // 什么也不做 }如此一来空的tuple也不会导致问题了。
虽然有些缺点,还需要工具类模板来实现遍历,但这是旧时代的c++实现for element in t的唯一选择。
使用编译期条件分支好消息是现在是现代c++的天下了,我们可以简化一下代码。
比如使用c++17提供的编译期间计算的条件分支。一般形式如下:
if constexpr (编译期常量表达式) { work 1 } else { work 2 }constexpr if最大的威力在于如果条件表达式为真,那么else里的语句根本不会被编译,反之亦然。当然这得是在模板里,否则else分支的代码仍然会被编译器检查代码的语法正确性。
没错,我们在最开始遇到的问题就是if和else里的语句都会被编译,导致了模板的无限递归,现在我们可以用constexpr if解决问题了:
template <typename Tuple, typename Functor, int Index> constexpr void for_each_tuple_impl(Tuple &&t, Functor &&f) { if constexpr (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> constexpr void for_each_tuple(Tuple &&t, Functor &&f) { for_each_tuple_impl<Tuple, Functor, 0>(std::forward<Tuple>(t), std::forward<Functor>(f)); }这次当遍历完最后一个元素后函数会触发退出递归的条件,if constexpr会帮我们终止模板的递归。问题被干净利落地解决了。
对于空tuple这个方案是如何处理的呢?答案是tuple为空的时候直接达到了impl的退出条件,所以是安全的noop。
虽然代码被进一步简化了,但是模板递归的两大问题依旧存在。
变长模板参数——错误的解法现代c++有许多简化模板元编程的利器。如果说前面的constexpr if是编译期计算和模板不沾边,那下面要介绍的变长模板参数可就是如假包换的模板技巧了。
顾名思义,变长模板参数可以让我们在模板参数上指定任意数量的类型/非类型参数:
template <typename... Ts> class tuple;上面的就是c++11中新增的tuple的定义,通过变长模板参数使得tuple支持了任意多的类型不同的元素。
想要处理变长模板参数,在c++17之前还是得靠模板递归。所以我们是不是可以用变长模板参数获取tuple里每一个元素的类型呢?正好get也可以根据元素的类型来获取相应的数据。
于是新的实现产生了:
template <typename Tuple, typename Functor, typename First, typename... Ts> constexpr void for_each_tuple2_impl(const Tuple& t, Functor &&f) { f(std::get<First>(t)); for_each_tuple2_impl<Tuple, Functor, Ts...>(t, std::forward<Functor>(f)); } template <typename Tuple, typename Functor> constexpr void for_each_tuple2_impl(const Tuple&, Functor &&) { return; } template <typename Functor, typename... Ts> constexpr void for_each_tuple2(const std::tuple<Ts...> &t, Functor &&f) { for_each_tuple2_impl<std::tuple<Ts...>, Functor, Ts...>(t, std::forward<Functor>(f)); }代码有些复杂,我会逐步讲解。