省略号在 C 和 C++ 中有多种用法。 其中包括函数的变量参数列表。 C 运行时库中的 printf()
函数是最著名的示例之一。
——Microsoft Learn
以下代码均在C++20标准下运行,若有CV工程师发现代码无法运行,请手动指定C++标准为>=20。
CMake下添加/更改此行。
set (CMAKE_CXX_STANDARD 20 )
形参包 声明 template <typename ... Types>
参数包与包展开
...
在参数名称的左侧,表示“参数包”,...
在参数名称的右侧,将参数包扩展为单独的名称。
template <typename ... Args>void f (Args... args) { f (args...); }
用例 仅仅看上面的语法会比较抽象,让我们通过实例来讲解。
不妨设现在有一需求,实现一函数,对传入函数的所有参数(参数有各种类型)进行加和,并返回结果。
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { return arg + sum (args...); }
这样写看起来非常完美,也酷似递归,但实际上略加思考就会发现,递归是有边界条件的,而这个函数却没有规定边界条件。
如是我们可以改为如下:
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { return arg + sum (args...); }
另一种写法:
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { if constexpr (sizeof ...(args) > 0 ) { return arg + sum (args...); } else { return arg; } }
关于sizeof… 我们会在后文介绍。
在上文中,请注意我说“酷似递归”,以及注释中“递归”的引号,因为这种形式实际上并不是严格意义上的递归,因为其调用的函数标签 不同。
在C++ Insights (cppinsights.io) 中,我们将代码段贴入,看看编译器是如何帮助我们处理的。
Source :
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { if constexpr (sizeof ...(args) > 0 ) { return arg + sum (args...); } else { return arg; } } int main () { auto res = sum (6.1 , 5 , 4 , 30 , 5.1 ); }
Insight :
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { if constexpr (sizeof ...(args) > 0 ) { return arg + sum (args... ); } else { return arg; } } #ifdef INSIGHTS_USE_TEMPLATE template <>double sum <double , int , int , int , double >(double arg, int __args1, int __args2, int __args3, double __args4){ if constexpr (true ) { return arg + sum (__args1, __args2, __args3, __args4); } else { } } #endif #ifdef INSIGHTS_USE_TEMPLATE template <>double sum <int , int , int , double >(int arg, int __args1, int __args2, double __args3){ if constexpr (true ) { return static_cast <double >(arg) + sum (__args1, __args2, __args3); } else { } } #endif #ifdef INSIGHTS_USE_TEMPLATE template <>double sum <int , int , double >(int arg, int __args1, double __args2){ if constexpr (true ) { return static_cast <double >(arg) + sum (__args1, __args2); } else { } } #endif #ifdef INSIGHTS_USE_TEMPLATE template <>double sum <int , double >(int arg, double __args1){ if constexpr (true ) { return static_cast <double >(arg) + sum (__args1); } else { } } #endif #ifdef INSIGHTS_USE_TEMPLATE template <>double sum <double >(double arg){ if constexpr (false ) { } else { return arg; } } #endif int main () { double res = sum (6.0999999999999996 , 5 , 4 , 30 , 5.0999999999999996 ); return 0 ; }
总结来说,在我们调用sum
时,template 为我们生成了<double, int, int, int, double>
,<int, int, int, double>
,<int, int, double>
,<int, double>
,<double>
,这五个特化模板函数。他们的函数参数也各不相同,并不符合递归的定义。
展开形式 依旧以上面的函数为例子
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { if constexpr (sizeof ...(args) > 0 ) { return arg + sum (args...); } else { return arg; } }
我们同样可以传入引用,或者限定符。
template <typename Arg, typename ... Args>auto sum (const Arg& arg, const Args&... args) { if constexpr (sizeof ...(args) > 0 ) { return arg + sum (args...); } else { return arg; } }
如此展开args
,得到的类型都是const Args&
,在处理基础类型时无需关心此类情况,但涉及自定义类型 struct
/class
时需要考虑值类别,而const T&
是可以接取右值的,它可以接取左值也可以接取右值,当然我们可以使用T&&
来生成万能引用。
实战 至此,我们可以自己封装一个类似CRT(C 运行时库)printf
的函数,且不需要格式限定符。
Code #include <iostream> namespace mylib { void printf () { std::cout << std::endl; } template <typename Arg> void printf (const Arg& arg) { std::cout << arg << std::endl; } template <typename Arg, typename ... Args> void printf (const Arg& arg, const Args&... args) { std::cout << arg << ',' ; printf (args...); } } int main () { mylib::printf ("Hello world!" , 5201314 , 3.1415926 , 'U' ); }
Console C:\Users\17253\CLionProjects\params\cmake-build-debug\params.exe Hello world!,5201314,3.14159,U 进程已结束,退出代码为 0
形参包索引
Pack indexing is a C++2c extension.
我们可以通过下标运算符[]
来实现对形参包特定索引下元素的访问。
已经验证,gcc/g++不支持此拓展,msvc暂不明确。
std::tuple
类模板 std::tuple
是固定大小的异质值的汇集。它是 std::pair
的泛化。
——cppreference
std::make_tuple
可创建一个 tuple
对象,其类型根据各实参类型定义。
借助std::tuple
,我们可以将参数包进行存储并访问。
std::get
可元组式访问指定的元素。
auto first_plus_second (auto ... args) { auto tuple = std::make_tuple (args...); return std::get <0 >(tuple) + std::get <1 >(tuple); }
我们不难想到一种意外情况:args参数包内的参数数量不足2时,我们使用std::get<1>(tuple)
会产生意想不到的后果。
因此我们需要获取并验证参数包的长度。
sizeof...
运算符与sizeof
运算符不同,注意不要混淆。
sizeof...
运算符用于获取形参包长度。
#include <iostream> auto first_plus_second (auto ... args) { static_assert (sizeof ...(args) >= 2 , "Parameters pack is to small to call this function, its size must be 2 at least." ); auto tuple = std::make_tuple (args...); return std::get <0 >(tuple) + std::get <1 >(tuple); } auto first_plus_last (auto ... args) { static_assert (sizeof ...(args) >= 2 , "Parameters pack is to small to call this function, its size must be 2 at least." ); auto tuple = std::make_tuple (args...); return std::get <0 >(tuple) + std::get <sizeof ...(args)-1 >(tuple); } int main () { std::cout << first_plus_second (1 , 3 , 5 ); std::cout << first_plus_second (1 ); }
折叠表达式 一元折叠 依旧回到我们的sum
函数代码并以此为例。
template <typename Arg, typename ... Args>auto sum (Arg arg, Args... args) { if constexpr (sizeof ...(args) > 0 ) { return arg + sum (args...); } else { return arg; } }
我们将其改为:
template <typename ... Args>auto sum (Args... args) { return (... + args); }
一下就简明了许多,发生了什么?首先我们需要参阅下表。
Syntax
Name
(形参包 运算符
… )
一元右折叠
( …运算符 形参包
)
一元左折叠
(形参包 运算符
…运算符 初值
)
二元右折叠
(初值 运算符
…运算符 形参包
)
二元左折叠
我们现在对其进行解释:
折叠表达式 的实例化按以下方式展开成表达式 E:
一元右折叠 (E
运算符 ...)
一元左折叠 (...
运算符 E)
二元右折叠 (E
运算符 ...
运算符 I)
二元左折叠 (I
运算符 ...
运算符 E)
template <typename ... Args>auto sum (Args... args) { return (... + args); }
二元折叠 那么关于二元折叠的情况呢?我们回到自己封装的printf
函数:
void printf () { std::cout << std::endl; } template <typename Arg>void printf (const Arg& arg) { std::cout << arg << std::endl; } template <typename Arg, typename ... Args>void printf (const Arg& arg, const Args&... args) { std::cout << arg << ',' ; printf (args...); }
注意到<<
为链式调用,且全局对象std::cout
为初值,即
那么我们可以参考二元左折叠 ,对我们的printf
进行改造:
template <typename ... Args>void printf (const Args&... args) { (std::cout << ... << args); }
Code :
#include <iostream> namespace mylib { template <typename ... Args> void printf (const Args&... args) { (std::cout << ... << args); } } int main () { mylib::printf ("Hello world!" , 5201314 , 3.1415926 , 'U' ); }
Console :
C:\Users\17253\CLionProjects\params\cmake-build-debug\params.exe Hello world!52013143.14159U 进程已结束,退出代码为 0
我们发现逗号消失了,这并不是我们想要的。
仔细观察,会发现我们需要的是
不妨化简为
发现其符合一元左折叠的形式,我们依此对printf
改造
Code :
#include <iostream> namespace mylib { template <typename ... Args> void printf (const Args&... args) { ((std::cout<< args << "," ), ...); std::cout<< std::endl; } } int main () { mylib::printf ("Hello world!" , 5201314 , 3.1415926 , 'U' ); }
Console :
C:\Users\17253\CLionProjects\params\cmake-build-debug\params.exe Hello world!,5201314,3.14159,U, 进程已结束,退出代码为 0
结果符合我们的预期。
普通展开 #include <iostream> namespace mylib{ void printf () { std::cout.operator <<(std::endl); } template <typename Arg> void printf (const Arg & arg) { (std::cout << arg) << std::endl; } #ifdef INSIGHTS_USE_TEMPLATE template <> void printf <char >(const char & arg) { std::operator <<(std::cout, arg).operator <<(std::endl); } #endif template <typename Arg, typename ... Args> void printf (const Arg & arg, const Args &... args) { (std::cout << arg) << ',' ; printf (args... ); } #ifdef INSIGHTS_USE_TEMPLATE template <> void printf <char [13 ], int , double , char >(const char (&arg)[13 ], const int & __args1, const double & __args2, const char & __args3) { std::operator <<(std::operator <<(std::cout, arg), ',' ); printf (__args1, __args2, __args3); } #endif #ifdef INSIGHTS_USE_TEMPLATE template <> void printf <int , double , char >(const int & arg, const double & __args1, const char & __args2) { std::operator <<(std::cout.operator <<(arg), ',' ); printf (__args1, __args2); } #endif #ifdef INSIGHTS_USE_TEMPLATE template <> void printf <double , char >(const double & arg, const char & __args1) { std::operator <<(std::cout.operator <<(arg), ',' ); printf (__args1); } #endif #ifdef INSIGHTS_USE_TEMPLATE template <> void printf <char >(const char & arg); #endif } int main () { mylib::printf ("Hello world!" , 5201314 , 3.1415926000000001 , 'U' ); return 0 ; }
折叠表达式 #include <iostream> namespace mylib{ template <typename ... Args> void printf (const Args &... args) { (((std::cout << args) << "," ) , ...); std::cout.operator <<(std::endl); } #ifdef INSIGHTS_USE_TEMPLATE template <> void printf <char [13 ], int , double , char >(const char (&__args0)[13 ], const int & __args1, const double & __args2, const char & __args3) { (std::operator <<(std::operator <<(std::cout, __args0), "," )) , ((std::operator <<(std::cout.operator <<(__args1), "," )) , ((std::operator <<(std::cout.operator <<(__args2), "," )) , (std::operator <<(std::operator <<(std::cout, __args3), "," )))); std::cout.operator <<(std::endl); } #endif } int main () { mylib::printf ("Hello world!" , 5201314 , 3.1415926000000001 , 'U' ); return 0 ; }
结论 不难看出普通展开生成了多个特化,折叠表达式仅生成一个。
因此我们可以得出结论:
折叠表达式在效率上高于普通展开。