C++20 新增支持在lambda表达式中,捕捉上下文使用初始化捕捉时,允许使用参数包方式进行初始化。这个说明的确比较拗口,我们先了解一下参数包和 lambda 的初始化捕捉的基本情况,再来看这个改动具体是做什么的。
参数包(parameter pack )
参数包是在定义模板时,支持可变数量的模板参数的一种技术,支持类模板和函数模板的可变参数,
template< typename ... VT > class Tuple { };Tuple<> t1; // <1> 不定数量类型参数,可以是0个类型参数Tuple t2; // <2> 可以是一个类型参数,int型Tuple t3; // <3> 可以是两个类型参数,int型和float型template< typename ... VT > void func( VT ... args );func( ); // <1> 相当于使用 void func( void );func( 1 ); // <2> 相当于使用 void func( int a1 );func( 2, 1.0 ); // <3> 相当于使用 void func( int a1, double a2 );
在使用参数包定义的变量时,常用的处理方式是直接参数包展开,或者使用展开表达式(fold expression):
template< typename ... VT, typename U > U sum( VT ... args ){ // <1> 如果有三个参数a1/a2/a3,展开后相当于: ( ( a1 + a2 ) + a3 ) U u1 = ( ... + args ); return u1;}
lambda 的初始化捕捉
lambda在捕捉上下文变量时,可以有两种捕捉方式:
- simple-capture,简单捕捉,直接按值或者按引用的方式进行捕捉
- init-capture,初始化捕捉,定义一个局部变量,并且进行初始化之后进行捕捉
int x1 = 4;int x2 = 5;auto y = [ &r = x1, x1 = x1+1, x2 ] () -> int { // <1> x2 是简单捕捉,r 和 x1 是初始化捕捉, // <2> 注意上一行中的三个 x1 不是同一个对象, // <3> 第一和第三个 x1 是引用外面的 x1 ,而第二个 x1 是定义内部使用的 x1 , r += 2; // <4> r 是引用外部的 x1 ,所以对 r 进行修改相当于对外部的 x1 进行修改 return x1 + x2 +2; // <5> 这里的 x1 是内部的 x1 ,初始化取值是 5 (即 4 + 1 ),不受上一行修改外部 x1 所影响} (); // <6> 定义一个lambda之后马上调用,执行完之后,外部 x1 的值为 6 ,y 的值为 12
lambda 初始化捕捉支持参数包
早期 C++ 标准中的 lambda 初始化捕捉,不支持参数包,主要是因为早期的标准中规定, lambda 的初始化捕捉时,定义的每一个变量,都需要在相关联的闭包类中,有相应标识符名称的非静态成员变量。
但是如果一个未展开的参数包可以是一个有名称的变量,那么会增加很多复杂性,外部无法判断一个变量是否是可以展开的参数包,也很难将参数包控制在模板内部,因此早期 C++ 中禁止这种写法。
后来 C++ 标准中进行了改进, lambda 的捕捉初始化时定义的每一个变量,不再要求在相应闭包类中有相应标识符名称的非静态成员变量,而是改为无名字的成员变量,这样这个限制就不再存在了,因此 C++20 中就允许 lambda 初始化捕捉支持参数包。
#include #include #include #include using std::cout, std::endl, std::string;// <1> 定义通用模板,使用static_cast<>方式转换返回int值template< typename T >int GetVal( T v ) { return static_cast( v ); } // <2> 定义字符串特化模板,将字符串转为int值template<>int GetVal( const char * v ) { return std::atoi( v ); } // <3> 定义字符串特化模板,将字符串转为int值template<>int GetVal( string v ) { if( v.empty() ) return 0; return std::stoi( v );}// <4> 传统的 C 语言方式的可变参数,运行时获取可变参数信息int sum_int( int num, ... ) { std::va_list va; va_start( va, num ); int i = 0; int sum = 0; for( i = 0; i < num; ++i ) sum += va_arg( va, int ); return sum;} // <5> C++模板的参数包方式的可变参数,编译时展开可变参数信息template< typename ... VT > int Sum( VT ... args ) { // <6> 直接使用展开表达式 int a1 = ( ... + GetVal( args ) ); // <7> lambda中通过简单捕捉方式,按值使用可变参数 int a2 = [ args ... ] { return ( ... + GetVal( args ) ); } (); // <8> C++17不支持,C++20才支持, // lambda中通过捕捉初始化,按移动的方式使用可变参数 int a3 = [ ... newargs = std::move( args ) ] { // <9> 只是展示如何获取参数个数和简单展开包拓展, // 将编译期能处理的转为运行期再处理,未能发挥模板编译期处理的效率优势 return sum_int( sizeof ... ( newargs ), GetVal( newargs ) ... ); } (); // <10> 注意上面使用 std::move() 之后,args中的字符串被移走,变成空字符串了 int a4 = ( ... + GetVal( args ) ); cout << a1 << " " << a2 << " " << a3 << " " << a4 << endl; // <11> a1 、a2 、a3 的值都一样(5+6+7+8=26),但 a4 的值会小 8,因为字符串被移走了 return ( a1 + a2 + a3 ) / 3; }int main( int argc, char * argv[] ){ std::string str = "8"; int a1 {}; a1 = Sum( 5, 6.1, "7", str ); cout << a1 << endl; return 0;}
编译和运行结果为:
[smlc@test code]$ g++ -std=c++17 a14.cppa14.cpp: In function 'int Sum(VT ...)':a14.cpp:37:13: warning: pack init-capture only available with '-std=c++2a' or '-std=gnu++2a' 37 | int a3 = [ ... newargs = std::move( args ) ] { | ^~~[smlc@test code]$ g++ -std=c++20 a14.cpp[smlc@test code]$ ./a.out26 26 26 1826
【往期回顾】
C++20 新特性(13):无状态lambda可以构建和赋值
C++20 新特性(12):使用模板语法的泛型lambda