在上一篇关于元组迭代的文章中,我们介绍了基本知识。因此,我们实现了一个函数模板,它接受一个元组,并可以很好地将其打印到输出中。还有一个版本带有operator<&书信电报;。
今天我们可以进一步了解其他一些技术。第一个是来自C++17的std::apply,它是元组的辅助函数。今天的文章还将介绍一些策略,使迭代更通用,并处理定制的可调用对象,而不仅仅是打印。
这是小系列的第二部分。请参阅本文的第一篇文章,其中我们讨论了基础知识。
std::tuple的一个方便助手是C++17中的std::apply函数模板。它接受一个元组和一个可调用对象,然后使用从元组中获取的参数调用这个可调用对象。
#包括<;iostream>#包括<;tuple>;int sum(int a,int b,int c){返回a+b+c;}无效打印(std::string_视图a、std::string_视图b){std::cout<;<;<;<;<;<;a<;<;<;<;<;<;<;<;<;<;b<;<;<;<;<;<;<;<;<;)\n";;}int main(){std::tuple number{1,2,3};标准:cout<&书信电报;标准::应用(总和、数字)<<'\n';std::tuple strs{";Hello";,";World";};std::应用(打印,strs);}
如您所见,std::apply接受sum或print函数,然后“展开”元组,并使用适当的参数调用这些函数。
关键是std::apply隐藏了所有索引生成和对std::get<>;。这就是为什么我们可以用std::apply替换打印函数,然后不使用index_序列。
我想到的第一种方法是:创建一个变量函数模板,该模板采用Args。。。并将其传递给std::apply:
模板<;字体名。。。Args>;void printImpl(const Args&;tupleArgs){size_t index=0;auto printElem=[&;index](const auto&;x){if(index++>;0)std::cout<;<;<;<;<;34;>;std::cout<;<;x;(普列特莱姆(图普里格斯),…);]模板<;字体名。。。Args>;void printTupleApplyFn(const std::tuple<;Args…>;>;>;{std::cout<;<;<;<;<;<;<;<;>;>;>;<;<;>;>;>;>;>;>;>;[34;]
问题在于out printImpl是一个可变函数模板,因此编译器必须实例化它。当我们调用std::apply时,实例化不会发生,而是在std::apply内部发生。当我们调用std::apply时,编译器不知道如何调用可调用对象,因此在这个阶段它无法执行模板推导。
#包括<;iostream>#包括<;tuple>;模板<;字体名。。。Args>;void printImpl(const Args&;tupleArgs){size_t index=0;auto printElem=[&;index](const auto&;x){if(index++>;0)std::cout<;<;<;<;<;34;>;std::cout<;<;x;(普列特莱姆(图普里格斯),…);]模板<;字体名。。。Args>;void printTupleApplyFn(const std::tuple<;Args…>;>;tp){std::cout<;<;<;<;<;<;<;<;34;(>;std::apply(printImpl<;Args…>;,tp);/<;std::cout<;<;<;<;<;>;)和int main(){std::tuple tp{10,20,3.14};printTupleApplyFn(tp);}
在上面的示例中,我们帮助编译器创建了请求的实例化,因此它很乐意将其传递给std::apply。
struct HelperCallable{template<;typename…Args>;void操作符()(const Args&;tupleArgs){size_t index=0;auto printElem=[&;index](const auto&;x){if(index++>;0)std::cout<;<;<;<;34;>;std::cout<;<;x;};(普列特莱姆(图普里格斯),…);};模板<;字体名。。。Args>;void printTupleApplyFn(const std::tuple<;Args…>;>;>;{std::cout<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>;>
现在,我们所做的,只是传递一个可帮助调用的对象;它是一个具体的类型,因此编译器可以毫无问题地传递它。没有模板参数推导。然后,在某个时刻,编译器将调用HelperCallable(args…),调用该结构的运算符()。现在一切都很好,编译器可以推断出类型。换句话说,我们推迟了问题的解决。
因此,我们知道代码可以与助手可调用类型配合使用……那么lambda呢?
#包括<;iostream>#包括<;tuple>;模板<;typename TupleT>;void printtupleappy(const-TupleT&;tp){std::cout<;<;<;<;34;(";std::apply([](const-auto&;…tupleArgs){size_-t index=0;auto-printElem=[&;index](const-auto&;x){if(index++gt;0)std::cout<;<;<;<;<;<;>;34;>;>;(普列特莱姆(图普里格斯),…);},tp)标准::cout<<")" ; } intmain(){std::tuple tp{10,20,3.14,42和#34;hello和#34;};printTupleApply(tp);}
正如你所看到的,我们在一个lambda中有一个lambda。它类似于我们使用运算符()的自定义类型。您还可以通过C++透视图来了解转换:这个链接
因为我们的可调用对象得到一个可变参数列表,所以我们可以使用这些信息,使代码更简单。
内部lambda中的代码使用索引检查是否需要打印分隔符——它检查是否打印第一个参数。我们可以在编译时执行此操作:
#包括<;iostream>#包括<;tuple>;模板<;typename TupleT>;void printTupleApply(const-TupleT&;tp){std::apply([](const-auto&;first,const-auto&;restArgs){auto-printElem=[](const-auto&;x){std::cout<;<;<;<;<;<;<;x;};标准:cout<<"(";<;<;第一;(printElem(restArgs),…);),tp);标准:cout<<")" ; } intmain(){std::tuple tp{10,20,3.14,42和#34;hello和#34;};printTupleApply(tp);}
这段代码在元组没有元素时会中断——我们可以通过在if constexpr中检查它的大小来修复这一问题,但现在让我们跳过它。
到目前为止,我们主要关注打印元组元素。所以我们有一个“固定”函数,每个参数都被调用。为了进一步阐述我们的想法,让我们尝试实现一个接受通用可调用对象的函数。例如:
模板<;typename TupleT,typename Fn,std::size\u t。。。Is>;每个元组无效(元组&;tp,Fn&;Fn,标准::索引序列<;Is…>;){(fn(std::get<;Is>;(std::forward<;TupleT>;(tp)),…);}模板<;typename TupleT,typename Fn,std::size\u t TupSize=std::tuple\u size\u v<;标准::移除_cvref_t<;TupleT>>>;对于每个元组无效(TupleT&;tp,Fn&;Fn){对于每个元组impl(std::forward<;TupleT>;(tp),std::forward<;Fn>;(Fn),std::make_index_sequence<;TupSize>;{}
首先,代码使用通用引用(转发引用)传递元组对象。这是支持各种用例所必需的,尤其是当调用方想要修改元组中的值时。这就是为什么我们需要在所有地方使用std::forward。
这是C++20 trait中的一个新助手类型,它确保我们从通过通用引用获得的类型中获得“真实”类型。
下面是一个关于元组迭代链接到Stackoverflow的问题的很好的总结:
作为T&&;是转发引用,T将是tuple<>&;或者tuple<>;康斯特;当左值传入时;但是std::tuple_size只适用于tuple<>;,所以我们必须去掉引用和可能的常量。在C++20添加std::remove_cvref_t之前,使用decation_t是一种简单(如果过度杀戮)的解决方案。
我们讨论了一个索引序列的实现;我们也可以在std::apply中尝试同样的方法。它能产生更简单的代码吗?
模板<;typename TupleT,typename Fn>;每一个tuple2(TupleT&;amp;tp,Fn&;Fn){std::apply([&;Fn](auto&;…args){(Fn(args),…);}无效,标准:前进<;TupleT>;(tp);}
模板<;typename TupleT,typename Fn>;对于每个tuple2(TupleT&;tp,Fn&;Fn){std::apply([&;Fn]<;typename…T>;(T&;…args){(Fn(std::forward<;T>;(args)),…)},无效,标准:前进<;TupleT>;(tp);}
此外,如果要坚持使用C++17,可以对参数应用decltype:
模板<;typename TupleT,typename Fn>;每一个tuple2(TupleT&;tp,Fn&;Fn){std::apply([&;Fn](auto&;…args){(Fn(std::forward<;decltype(args)>;(args)),…);},标准:前进<;TupleT>;(tp);}
背景任务是打印元组元素,并找到转换它们的方法。在这个过程中,我们经历了变量模板、索引序列、模板参数推导规则和技巧、std::apply和删除引用。
我很高兴讨论变化和改进。请在下面的评论中告诉我你的想法。
C++模板:David Vandevoorde、Nicolai M. Josuttis、Douglas Gregor的完整指南(第二版)
我';如果你';对现代C++有兴趣!了解最近C++标准的所有主要特征!看看这里: