C++----lambda表达式、完美转发

lambda表达式、完美转发在c++知识点中,属于难以理解的板块。初学者不建议详细学此部分,有一点的开发基础可以再回来深究。


C++ ----lambda表达式

lambda 源自希腊字母表中第 11 位的 λ,在计算机科学领域,它则是被用来表示一种匿名函数。所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。

使用方法

[外部变量访问方式说明符] (参数) mutable noexcept -> 返回值类型
{
  函数体;
};

各部分的含义分别为:

  • [外部变量访问方式说明符]
    [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。

所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。

[]不捕获任何变量

[&]捕获外部作用域中所有变量,并作为引用在函数体重使用

[=]捕获外部作用域中所有变量,并作为副本在函数体重使用

[=,&foo]捕获外部作用域中所有变量,并作为副本在函数体重使用,对于foo按引用捕获

[foo]当作副本捕获foo,不引入其他变量

[this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,则默认有此选项。捕获this的目的是在lambda中使用当前类的成员函数以及成员变量。
详细见下文:
在这里插入图片描述

  • (参数)
    和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;

  • mutable
    可选,如果使用则之前的 () 小括号不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字

  • noexcept
    可选,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常

  • -> 返回值类型
    可选,指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型

  • 函数体
    和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量

去掉可选的,如下就定义了一个最简单的 lambda 匿名函数:

[]{}

显然,此 lambda 匿名函数未引入任何外部变量([] 内为空),也没有传递任何参数,没有指定 mutable、noexcept 等关键字,没有返回值和函数体。所以,这是一个没有任何功能的 lambda 匿名函数。

外部变量捕获方式表

对于 lambda 匿名函数的使用,令多数初学者感到困惑的就是 [外部变量] 的使用。其实很简单,无非下表所示的这几种格式。

外部变量格式功能
[]空方括号表示当前 lambda 匿名函数中不导入任何外部变量。
[=]只有一个 = 等号,表示以值传递的方式导入所有外部变量;
[&]只有一个 & 符号,表示以引用传递的方式导入所有外部变量;
[val1,val2,…]表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;
[&val1,&val2,…]表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;
[val,&val2,…]以上 2 种方式还可以混合使用,变量之间没有前后次序。
[=,&val1,…]表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
[this]表示以值传递的方式导入当前的 this 指针。

注意,单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。

实例:lambda 匿名函数的定义和使用。

template<typename T,typename FUN>
void sort(T* arr,size_t len, FUN cmp)
{
	for (size_t i = 0; i < len - 1; i++)
	{
		for (size_t k = 0; k < len -i -1; k++)
		{
			if (cmp(&arr[k],&arr[k + 1]))
			{
				std::swap(arr[k], arr[k + 1]);
			}
		}
	}
}
int main()
{
	int arr[5] = { 8,7,5,6,9 };
	sort(arr, 5, [](int* pa, int* pb)
		{
			return *pa < *pb;
		});
	for (int n : arr)
	{
		cout << n << " ";
	}

	return 0;
}

如果使用函数指针,那么需要先定义一个函数,再传指针,显然在类似的场景中,使用 lambda 匿名函数更有优势。虽然 lambda 匿名函数没有函数名称,但我们仍可以为其手动设置一个名称,比如:

//display 即为 lambda 匿名函数的函数名
auto display = [](int a) {cout << "lambda " << a << endl; };
//调用 lambda 函数
display(222);

可以看到,程序中使用 auto 关键字为 lambda 匿名函数设定了一个函数名,由此我们即可在作用域内调用该函数。

完美转发

C++11 标准为 C++ 引入右值引用语法的同时,还实现了简单的方式即可在函数模板中实现参数的完美转发。那么,什么是完美转发?
首先解释一下什么是完美转发,它**指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。**所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

举个例子:

template<typename T>
void function(T t) 
{    
    otherdef(t);
}

如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。

显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。

有人可能会问,完美转发这样严苛的参数传递机制,很常用吗?C++98/03 标准中几乎不会用到,但 C++11 标准为 C++ 引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。

C++ 11 标准中允许在函数模板中使用右值引用来实现完美转发。

C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。

仍以 function() 函数为例,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:

template <typename T>
void function(T&& t) 
{    
    otherdef(t);
}

此模板函数的参数 t 既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参,如下所示:

int n = 10;
int & num = n;
function(num); // T 为 int&
int && num2 = 11;
function(num2); // T 为 int &&

其中,由 function(num) 实例化的函数底层就变成了 function(int & & t),同样由 function(num2) 实例化的函数底层则变成了 function(int && && t)。要知道,C++98/03 标准是不支持这种用法的,而 C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):

  • 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
  • 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。

只需要知道,在实现完美转发时,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。

通过**将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。**但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,**对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。**那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?

C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 ** forword()(接收到的形参连同其左、右值属性,一起传递给被调用的函数) **,我们只需要调用该函数,就可以很方便地解决此问题。仍以 function 模板函数为例,如下演示了该函数模板的用法:

#include <iostream>
using namespace std;//重载被调用函数,查看完美转发的效果
void otherdef(int & t) 
{    
    cout << "lvalue\n";
}
void otherdef(const int & t) 
{    
    cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) 
{    
    otherdef(forward<T>(t));
}
int main()
{    
    function(5);    
    int  x = 1;    
    function(x);    
    return 0;
}
/*程序执行结果为:
	rvalue
	lvalue
*/

注意程序中
template
void function(T&& t)
{
otherdef(forward(t));
}
此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Warm wolf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值