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

然而当你满心欢喜地准备尝试运行自己杰作的时候,编译器给你浇了一头冷水:

... /usr/include/c++/10.2.0/tuple:1259:12: fatal error: template instantiation depth exceeds maximum of 900 (use '-ftemplate-depth=' to increase the maximum) 1259 | struct tuple_element<__i, tuple<_Head, _Tail...> > | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ compilation terminated.

报了一大堆错,甚至超过了屏幕的最大滚动高度(我设置的是10000行)。发生了什么呢?

稍微翻翻报错信息,我们发现了实际上是模板递归超过了允许的最大深度。可是我们不是已经给出了退出递归的条件了吗?

让我再来看看impl的代码:

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)); } }

编译器在编译函数的时候是需要把所有条件分支都编译的,所以即使是在函数模板的实例达到退出递归的条件,else分支仍然会被编译,而在这个分支里模板会被不断递归实例化,最终超过允许的最大递归深度。

这里就引出了模板递归的一个重要规则:我们应该用模板特化或是函数重载来实现递归的终止条件

然而在这里我们既不是模板的特化也没有调用重载函数。

如果想利用函数重载的话并不现实,因为递归函数调用的形式是相同的,无法针对tuple的最后一个元素进行特殊处理。

而函数模板不支持部分特化,所以我们也很难实现一个针对tuple结尾的特化版本。

那怎么办呢?

通用的古典实现

既然函数模板不能满足要求,我们使用类模板不就行了。只要重载了operator(),使用起来也没多少区别。

所以一个真正通用的古典实现可以写出下面这样:

template <typename Tuple, typename Functor, std::size_t Start, std::size_t End> struct classic_for_each_tuple_helper { constexpr void operator()(const Tuple &t, Functor &&f) const { f(std::get<Start>(t)); classic_for_each_tuple_helper<Tuple, Functor, Start + 1, End>{}(t, std::forward<Functor>(f)); } };

我们首先实现了主模板,其中Start和End是tuple开始和结束的索引。每处理一个元素,我们就让Start加上1。

你可以想一想这里递归的停止条件是什么。

我们每次给Start递增1,那么最后我们的Start一定会等于甚至超过End。没错,这就是我们的停止条件:

template <typename Tuple, typename Functor, std::size_t End> struct classic_for_each_tuple_helper<Tuple, Functor, End, End> { constexpr void operator()(const Tuple &t, Functor &&f) const { f(std::get<End>(t)); } };

我们没办法在模板参数列表里判断相等,那么最好的解决办法就是特化出Start和End都一样的特殊情况,这时候用一样的值End同时填入主模板的Start和End就行了。

特化的处理也很简单,我们直接把递归的语句删了就可以了。

想要使用这个帮助模板还需要一点代码,因为我可不想每次手动指定一大长串的tuple类型参数。

正好,利用函数模板我们可以自动进行类型推导:

template <typename Tuple, typename Functor> constexpr void classic_for_each_tuple(const Tuple &t, Functor &&f) { classic_for_each_tuple_helper<Tuple, Functor, 0, std::tuple_size_v<Tuple> - 1>{}(t, std::forward<Functor>(f)); }

这样我们就可以书写如下的代码了:

classic_for_each_tuple(std::make_tuple(1, 2, 3, "hello", "world", 3.1415, 2.7183), [](const auto &element) { /* work */ });

即使是make_tuple生成的临时对象,我们也可以自动推导出它的类型,所有粗活累活编译器都帮我们代劳了。

不过凡事总是有代价的,有得必有失。表面上我们实现了简单而漂亮的接口,但代价实际上是被转移到了底层:

$ nm a.out | grep classic_for_each_tuple_helper 00000000000031d6 t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm0ELm6EEclERKS3_OS7_ 00000000000034f0 t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm1ELm6EEclERKS3_OS7_ 00000000000036ca t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm2ELm6EEclERKS3_OS7_ 0000000000003946 t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm3ELm6EEclERKS3_OS7_ 0000000000003a66 t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm4ELm6EEclERKS3_OS7_ 0000000000003b94 t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm5ELm6EEclERKS3_OS7_ 0000000000003c0e t _ZNK29classic_for_each_tuple_helperISt5tupleIJiiiPKcS2_ddEEZ4mainEUlRKT_E2_Lm6ELm6EEclERKS3_OS7_

我们的tuple有6个元素,所以我们生成了6个helper的实例。过多的模板实例会导致代码膨胀。

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

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