前言
如果你是一名c++开发者,或者仅仅是一名c++刚入门的小白,都应该去了解Lambda表达式的用法和它的魅力。c++和Lambda表达式的相遇,会给我们带来意想不到的惊喜。
一、什么是Lambda表达式?
Lambda 表达式是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的Lambda抽象,是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
二、为什么要使用Lambda表达式?
我们知道“Lambda 表达式”是一段可以传递的代码,因此它可以被执行一次或多次,我们将Lambda表达式理解为一个匿名函数, Lambda表达式允许将一个函数作为另外一个函数的参数。 也可以把 Lambda 表达式理解为是一段可以传递的代码(将代码作为实参),也可以理解为函数式编程,将一个函数作为参数进行传递。
三、Lambda表达式有什么特点?
- 可选的类型声明 - 不需要声明参数的类型。编译器从参数的值推断出相同的结果。
- 参数周围的可选括号 - 无需在括号中声明单个参数。对于多个参数,需要括号。
- 可选的花括号 - 如果主体包含单个语句,则无需在表达式主体中使用花括号。
- 可选的返回关键字 - 如果主体有一个返回值的表达式,编译器将自动返回该值。需要大括号来表示 expression 返回一个值。
四、接下来让我们一起来探索c++ lambda表达式的那些优美操作
Lambda表达式的开头
Lambda表达式的开头是以[]开始的,这个方括号括起的部分叫做捕获表达式,可以实现不同捕获上文变量的效果。
Lambda表达式常用格式
[捕获列表] (参数列表) {函数体}
定义打印一个整型参数的匿名函数可以这样编写和调用:
#include<bits/stdc++.h>
using namespace std;
int main()
{
[](int x){cout<<x<<endl;}(123456);
}
输出结果为:
123456
如果明确给出返回值类型,可以用“-> 返回值类型”的格式插入尾置返回类型 ,如:
[capture](parameters) mutable ->return-type{statement}
1.[capture]:捕捉列表。捕捉列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用。
2.(parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
3.mutable:mutable修饰符。默认情况下,Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
4.->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
5.{statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
其中除了“[ ]”(其中捕获列表可以为空)和“复合语句”(相当于具名函数定义的函数体),其它都是可选的。它的类型是单一的具有成员operator()的非联合的类型,称为闭包类型(closure type)。
插入尾置返回类型模板:
[捕获列表] (参数列表) -> 返回值类型 {函数体}
下面我们写一段代码观察一下:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int zhu = 0;
zhu = [](int x, int y)->int{if(x>y){return x;}else{return y;}}(10,100);
cout<<"最大的数是:"<<zhu<<endl;
}
输出结果
最大的数是:100
1.[捕获列表] 的用法与说明
与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在“[]”包括起来的是捕捉列表,捕捉列表由多个捕捉项组成,并以逗号分隔。捕捉列表有以下几种形式:
- [var] 表示以值传递方式捕捉变量var
- [=] 表示值传递捕捉所有父作用域变量
- [&var] 表示以引用传递方式捕捉变量var
- [&] 表示引用传递捕捉所有父作用域变量
- [this] 表示值传递方式捕捉当前的this指针
也可以组合使用,例如[=,&a] 表示以引用传递方式捕捉a,值传递方式捕捉其他变量。
另外,还可以特殊指定具体变量使用何种方式捕获,例如:
[x, &y]:变量x以赋值的方式被捕获, 变量y以引用的方式被捕获
[&, x]:以引用的方式捕获所有的外部变量,除了变量x用赋值的方式
例如可以将比较两个整数返回最小值的函数按如下方式编写和调用(注意,这两个整数的值都来自匿名函数之外的变量):
#include<bits/stdc++.h>
using namespace std;
int main()
{
int x = 100, y = 10;
int zhu = 0;
zhu = [x, y]()->int{if(x<y){return x;}else{return y;}}();
cout<<"最小的数是:"<<zhu<<endl;
}
输出结果:
最小的数是:10
如果我们想在匿名函数中改变变量x,y的值,可以在捕获时用引用的方式[&x, &y]
#include<bits/stdc++.h>
using namespace std;
int main()
{
int x = 100, y = 10;
int zhu = 0;
zhu = [&x, &y]()->int{x=99;y=9;if(x<y){return x;}else{return y;}}();
cout<<"最小的数是:"<<zhu<<endl;
cout<<"x:"<<x<<endl;
cout<<"y:"<<y<<endl;
}
输出结果:
最小的数是:9
x:99
y:9
2.Lambda表达式通过函数指针调用
c++ Lambda表达式和普通函数的用法看起来没有太大的区别,那么我们猜想:Lambda表达式可以通过函数指针去调用。
#include<bits/stdc++.h>
using namespace std;
int main()
{
void(* x)() = [ ]{
cout<<"我是上进小菜猪"<<endl;
};
x();
return 0;
}
如图所示,调用成功!说明我们的猜想正确,Lambda表达式可以通过函数指针去调用。
3.Lambda实现新的SFINAE方法
模板类中两个重载成员函数实现或者模板类的偏特化实现。
template<typename F,typename ... Args,
typename = decltype(std::declval<F>()(std::declval<Args&&>()...))>
std::true_type IsValid_Impl(void*);
template<typename F, typename ... Args>
std::false_type IsValid_Impl(...);
inline constexpr auto IsValid = [](auto functor) {
return [](auto &&... params) {
return decltype(IsValid_Impl<decltype(functor), decltype(params) &&...>(nullptr)){};
};
};
template<typename T>
struct TypeT {};
template<typename T>
constexpr auto type = TypeT<T>{};
template<typename T>
T ValueT(TypeT<T>);
constexpr auto IsDefaultConstructible = IsValid([](auto x)->decltype((void)decltype(ValueT(x))()) {});
4.闭包类型
标准并没有说拷贝构造函数为delete,因此可以拷贝闭包,且其闭包类型相同。
具体可参考如下代码
int main() {
std::cout << std::boolalpha;
auto lb1 = []() {};
auto lb2 = lb1;
std::cout << "lb1 and lb2 type same: "
<< std::is_same<decltype(lb1), decltype(lb2)>::value << "\n";
return 0;
}
上述结果输出为true。
5.捕获
将捕获归为无状态捕获,有状态捕获两大类,相对应为无状态闭包和有状态闭包。
看一看当按引用捕获变量a时,编译器的行为,示例代码如下
int main() {
int a = 1;
auto lb1 = [&a]() { ++a; };
lb1();
std::cout << "a= " << a << "\n";
return 0;
}
上述代码能正常通过,且能输出正确的结果。
通过C++ Insights观察上述lambda表达式生成的代码,其结果如下
class __lambda_7_13
{
public:
inline void operator()() const
{
++a;
}
private:
int & a;
public:
__lambda_7_13(int & _a)
: a{_a}
{}
};
__lambda_7_13 lb1 = __lambda_7_13(__lambda_7_13{a});
6.Lambda表达式利用auto关键字和类型推导
由于Lambda的类型是单一的,不能通过类型名来显示声明对应的对象,但可以利用auto关键字和类型推导:
#include<bits/stdc++.h>
using namespace std;
int main()
{
auto px=[](char x[100]){cout<<x<<endl;};
px("上进小菜猪");
}
运行结果:
上进小菜猪
7.当Lambda表达式遇上排序代码
降序排序:不依赖a和b的具体类型
#include<bits/stdc++.h>
using namespace std;
int main()
{
sort( a, a+8, [](const auto& a,const auto& b){return a>b;} );
}
因为参数类型和函数模板参数一样可以被推导而无需和具体参数类型耦合,有利于重构代码。和使用auto声明变量的作用类似,它也允许避免书写过于复杂的参数类型。特别地,不需要指出参数类型使使用高阶函数变得更加容易。
8.Lambda表达式可以嵌套使用
auto f=[x](int a,int b){return a>x;};//x被捕获复制
int x=0, y=1;
auto g=[&](int x){return ++y;};//y被捕获引用,调用g后会修改y,需要注意y的生存期
bool(*fp)(int, int)=[](int a,int b){return a>b;};//不捕获时才可转换为函数指针
五、Lambda与c++的故事就讲到这里啦
Lambda表达式是现代C++在C ++ 11和更高版本中的一个新的语法糖 ,在C++11、C++14、C++17和C++20中Lambda表达的内容还在不断更新。我们应该追随更新的技术,不断的学习知识,不断的强大自己。
任务26.使用c++ lambda表达式,可以有哪些优美操作?