本文介绍c可变参数包的相关用法定义模板参数和函数参数如下代码所示可以定义一个接受可变参数的函数template typename... Args void func(Args... args) { //函数体 } //如果args是右值引用需要这样写:Args...代码中的Args是一个模板形参,args是一个函数形参可以用类似与func(1,hello,21454,std::string{world});这样的方式调用这个函数获取参数包长度用size_t size sizeof...(args)或者size_t size sizeof...(Args)都可以获得参数包里的参数数量并且Args的长度是在编译期就确定的所以可以和constexpr以及if constexpr结合使用,后面我们会用到参数包展开这个非常重要,后面我们所有使用args的方式都需要理解这个假定args参数包里包含arg1,arg2,arg3三个参数,那么args...会被展开成arg1,arg2,arg3func(args...); //等价于func(arg1,arg2,arg3);不止如此,如果我们将args...替换成pattern...也同样可以展开(std::cout args )...//这个代码本身不合法! //相当于下面的代码 (std::cout arg1 ),(std::cout arg2 ),(std::cout arg3 )当然这样写是不合法的,这里只是告诉大家args...可以换成某个带args的pattern...,这样接下来的内容会好理解一点args的使用递归解包为了便于介绍,实现一个print_all函数,功能是打印输入的所有参数templatetypename T void print_all(T first){//这个重载是为了创造递归终点 std::cout first std::endl; } templatetypename T,typename... Args void print_all(T first,Args... args){ std::cout first std::endl; print_all(args...); }代码很好理解,每一层递归都负责打印参数包内第一个参数,并且提供一个重载作为递归终点,防止无限递归如果c标准比较新的话,也可以把上面那个重载去掉,用lambda实现递归调用,但是这样做没什么用,因为从c17开始就有更好的方式了,我把实现贴出来作为一个思路template typename... Args void print_all(Args... args) { auto print_single [](this auto self, auto arg_first, auto... args_tail) { std::cout arg_first ; if constexpr (sizeof...(args_tail) 0) { self(args_tail...); } }; print_single(args...); }在函数内部定义一个lambda表达式,使用c23的this auto语法可以很方便地做到递归调用,用auto作为参数类型的lambda表达式在底层本身就是模板,所以可以适配每一次调用的args_tail的长度都不同为了有一个递归终点,使用if constexpr在编译期就计算出后面的条件,只有一个参数的时候条件不满足,后面的self()调用不会生成,避免编译错误初始化列表从c11开始可以使用初始化列表的方式template typename... Args void print_all(Args... args) { int dummy[] {(std::cout args , 0)...}; }使用int dummy[] {}这种初始化列表定义数组的时候{}里面的参数是严格按照从左到右的顺序执行的(expr,0)使用了c语言逗号表达式,先执行逗号左边的表达式,后执行右边的表达式,最后将逗号右边的值作为返回值(std::cout args , 0)...运用了先前提到的pattern...所以上述代码会被展开为:int dummy[] {(std::cout arg1 , 0),(std::cout arg2 , 0),(std::cout arg3 , 0)};初始化列表里的每一个元素都是(std::cout arg , 0)这个逗号表达式会先执行左边的std::cout arg ,后将右边的0作为整个表达式的值存入数组实质上是用初始化列表来确保打印参数的顺序,用逗号表达式的副作用来实现打印,dummy[]数组本身没有意义折叠表达式c17加入了折叠表达式语法如下:(args op ...) - arg1 op (arg2 op arg3) (... op args) - (arg1 op arg2) op arg3 (args op ... op init) - arg1 op (arg2 op (arg3 op init)) (init op ... op args) - ((init op arg1) op arg2) op arg3op是运算符(-*/,||等),args是参数包,同样可以换成pattern,后两种用法中的init是初始值,主要和加减乘除之类的一起用,最外层的括号是语法的一部分,不能省略!template typename... Args void print_all(Args... args) { (..., (std::cout args )); }(..., (std::cout args ))中的op是一个逗号运算符,被展开成((std::cout arg1 ),(std::cout arg2 )),(std::cout arg3 ),按逗号运算符的特点,三个参数按顺序依次输出再举一个计算参数和的例子template typename... Args int sum_all(Args... args) { int sum 0; sum (sum ... args); return sum; }调用sum_all(1, 2, 3, 4)可得到答案10c20 lambda初始化捕获除开上面三种常规用法外,有时会需要将参数包完美转发进的其他可调用对象templatetypename F,typename... Args void func(F f,Args... args) { std::functionvoid() task std::bind(std::forwardF(f), std::forwardArgs(args)...); //调用task或者将task转移 }通常来说我们会像上面的代码这样做,但是随着lambda的功能逐渐完善,更灵活的lambda能逐渐替换掉bind函数的功能,如果需要将参数包完美转发进lambda表达式中,可以使用c20新加入的,专为lambda初始化捕获的语法,代码如下:templatetypename F,typename... Args void func(F f,Args... args) { std::functionvoid() task [f std::forwardF(f), ... args std::forwardArgs(args)]() { f(args...); }; //调用task或者将task转移 }... args std::forwardArgs(args)这种前缀的...语法相当于对参数包里的每一个参数都执行一次初始化捕获(这里是std::forward()也可以换成其他函数),再将捕获后的各个参数再打包成args总结在c17以上的环境,折叠表达式是最好的用法,简洁且高效需要将参数包转移到lambda内部的时候,通常来讲可以直接[args]或者[args]直接捕获,但是如果要保留移动语义实现完美转发或者需要其他的初始化措施的话,c20的[...argsstd::forwardArgs(args)]这种语法更高效如果环境没有c20,又需要完美转发,可以用auto args_tuple std::make_tuple(std::forwardArgs(args)...)然后将元组move进lambda表达式
c++可变参数包语法解析
本文介绍c可变参数包的相关用法定义模板参数和函数参数如下代码所示可以定义一个接受可变参数的函数template typename... Args void func(Args... args) { //函数体 } //如果args是右值引用需要这样写:Args...代码中的Args是一个模板形参,args是一个函数形参可以用类似与func(1,hello,21454,std::string{world});这样的方式调用这个函数获取参数包长度用size_t size sizeof...(args)或者size_t size sizeof...(Args)都可以获得参数包里的参数数量并且Args的长度是在编译期就确定的所以可以和constexpr以及if constexpr结合使用,后面我们会用到参数包展开这个非常重要,后面我们所有使用args的方式都需要理解这个假定args参数包里包含arg1,arg2,arg3三个参数,那么args...会被展开成arg1,arg2,arg3func(args...); //等价于func(arg1,arg2,arg3);不止如此,如果我们将args...替换成pattern...也同样可以展开(std::cout args )...//这个代码本身不合法! //相当于下面的代码 (std::cout arg1 ),(std::cout arg2 ),(std::cout arg3 )当然这样写是不合法的,这里只是告诉大家args...可以换成某个带args的pattern...,这样接下来的内容会好理解一点args的使用递归解包为了便于介绍,实现一个print_all函数,功能是打印输入的所有参数templatetypename T void print_all(T first){//这个重载是为了创造递归终点 std::cout first std::endl; } templatetypename T,typename... Args void print_all(T first,Args... args){ std::cout first std::endl; print_all(args...); }代码很好理解,每一层递归都负责打印参数包内第一个参数,并且提供一个重载作为递归终点,防止无限递归如果c标准比较新的话,也可以把上面那个重载去掉,用lambda实现递归调用,但是这样做没什么用,因为从c17开始就有更好的方式了,我把实现贴出来作为一个思路template typename... Args void print_all(Args... args) { auto print_single [](this auto self, auto arg_first, auto... args_tail) { std::cout arg_first ; if constexpr (sizeof...(args_tail) 0) { self(args_tail...); } }; print_single(args...); }在函数内部定义一个lambda表达式,使用c23的this auto语法可以很方便地做到递归调用,用auto作为参数类型的lambda表达式在底层本身就是模板,所以可以适配每一次调用的args_tail的长度都不同为了有一个递归终点,使用if constexpr在编译期就计算出后面的条件,只有一个参数的时候条件不满足,后面的self()调用不会生成,避免编译错误初始化列表从c11开始可以使用初始化列表的方式template typename... Args void print_all(Args... args) { int dummy[] {(std::cout args , 0)...}; }使用int dummy[] {}这种初始化列表定义数组的时候{}里面的参数是严格按照从左到右的顺序执行的(expr,0)使用了c语言逗号表达式,先执行逗号左边的表达式,后执行右边的表达式,最后将逗号右边的值作为返回值(std::cout args , 0)...运用了先前提到的pattern...所以上述代码会被展开为:int dummy[] {(std::cout arg1 , 0),(std::cout arg2 , 0),(std::cout arg3 , 0)};初始化列表里的每一个元素都是(std::cout arg , 0)这个逗号表达式会先执行左边的std::cout arg ,后将右边的0作为整个表达式的值存入数组实质上是用初始化列表来确保打印参数的顺序,用逗号表达式的副作用来实现打印,dummy[]数组本身没有意义折叠表达式c17加入了折叠表达式语法如下:(args op ...) - arg1 op (arg2 op arg3) (... op args) - (arg1 op arg2) op arg3 (args op ... op init) - arg1 op (arg2 op (arg3 op init)) (init op ... op args) - ((init op arg1) op arg2) op arg3op是运算符(-*/,||等),args是参数包,同样可以换成pattern,后两种用法中的init是初始值,主要和加减乘除之类的一起用,最外层的括号是语法的一部分,不能省略!template typename... Args void print_all(Args... args) { (..., (std::cout args )); }(..., (std::cout args ))中的op是一个逗号运算符,被展开成((std::cout arg1 ),(std::cout arg2 )),(std::cout arg3 ),按逗号运算符的特点,三个参数按顺序依次输出再举一个计算参数和的例子template typename... Args int sum_all(Args... args) { int sum 0; sum (sum ... args); return sum; }调用sum_all(1, 2, 3, 4)可得到答案10c20 lambda初始化捕获除开上面三种常规用法外,有时会需要将参数包完美转发进的其他可调用对象templatetypename F,typename... Args void func(F f,Args... args) { std::functionvoid() task std::bind(std::forwardF(f), std::forwardArgs(args)...); //调用task或者将task转移 }通常来说我们会像上面的代码这样做,但是随着lambda的功能逐渐完善,更灵活的lambda能逐渐替换掉bind函数的功能,如果需要将参数包完美转发进lambda表达式中,可以使用c20新加入的,专为lambda初始化捕获的语法,代码如下:templatetypename F,typename... Args void func(F f,Args... args) { std::functionvoid() task [f std::forwardF(f), ... args std::forwardArgs(args)]() { f(args...); }; //调用task或者将task转移 }... args std::forwardArgs(args)这种前缀的...语法相当于对参数包里的每一个参数都执行一次初始化捕获(这里是std::forward()也可以换成其他函数),再将捕获后的各个参数再打包成args总结在c17以上的环境,折叠表达式是最好的用法,简洁且高效需要将参数包转移到lambda内部的时候,通常来讲可以直接[args]或者[args]直接捕获,但是如果要保留移动语义实现完美转发或者需要其他的初始化措施的话,c20的[...argsstd::forwardArgs(args)]这种语法更高效如果环境没有c20,又需要完美转发,可以用auto args_tuple std::make_tuple(std::forwardArgs(args)...)然后将元组move进lambda表达式