Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客: <但凡.
我的专栏: 《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
欢迎点赞,关注!
在C++11及后续版本中,Lambda表达式作为一种革命性的特性被引入,极大地改变了我们编写函数对象的方式。 Lambda表达式不仅使代码更加简洁易读 ,还为STL算法 、多线程编程等场景提供了极大的便利。本文将全面剖析C++中Lambda表达式的语法、特性、应用场景及最佳实践。
目录
[1、 lambda](#1、 lambda)
1、 lambda
1.1、lambda表达式
**lambda本质上是一个匿名函数对象。它可以直接定义在函数内部。我们可以简单的理解为更灵活的函数。**我们可以快速简洁的定义和使用。以下是lambda表达式的规范格式:
[capture-list] (parameters) -> return type { function boby }
其中, [capture-list]是捕捉列表 ,这个列表出现在lambda函数 的开始位置。编译器根据[]来判断接下来的代码是否为lambda函数。捕捉列表可以传值或传引用捕捉。捕捉列表可以为空不能省略。
(parameters)是参数列表,跟普通函数的参数列表一样。我们要是不需要传参可以省略不写()。
-> return type是返回类型。当没有返回值时可以省略。返回值特定时也可以省略,由编译器推导函数返回值类型。
{ function boby } 就是函数体,和普通函数一样。
cpp
#include<iostream>
using namespace std;
int main()
{
auto add = [](int x, int y)->int {return x + y;};
cout << add(1, 2) << endl;//3
int a = 10;
auto add1 = [a](int x, int y)->int {return x + y + a;};//传值捕捉
cout << add1(1, 2) << endl;//13
auto add2 = [&a](int x, int y)->int {return x + y + a;};//传引用捕捉
cout << add2(1, 2) << endl;//13
return 0;
}
1.2、捕捉列表
如果我们lambda表达式想用函数体外的变量就需要自行捕捉。当然如果你想在函数体内用的对象是全局的就不用捕捉了。
接下来我们介绍三种捕捉方式,首先是第一种,显示的传值或传引用捕捉
在1.1的代码我们已经展示过传值捕捉和传引用捕捉了。如果想捕捉多个变量需要用逗号隔开。
cpp
auto add2 = [a,b,&c](int x, int y)->int {return x + y + a+b+c;};//a,b传值捕捉,c传引用捕捉
第二种捕捉方式是在捕捉列表隐式捕捉。这种捕捉方式需要我们在[]中写一个=或者&,然后函数体中用什么我们自动捕捉什么变量:
cpp
auto add2 = [=](int x, int y)->int {return a+b+c;};//隐式值捕捉
auto add3 = [&](int x, int y)->int {return a + b + c;};//隐式引用捕捉
第三种捕捉方式是混合使用隐式捕捉和显示捕捉。我们[]中的第一个元素必须是=或者&,表示是值捕捉或者引用捕捉,接下来的每个参数直接传变量名,他们的捕捉方式和第一个元素传的捕捉方式相反:
cpp
auto add2 = [=,&b,&c](int x, int y)->int {return a+b+c;};//a是传值捕捉,b,c是传引用捕捉
auto add3 = [&,b,c](int x, int y)->int {return a + b + c;};//a是传引用捕捉,b,c是传值捕捉
捕捉列表中不能捕捉全局变量,如果lambda表达式定义在全局中,捕捉列表必须为空。
labmda捕捉列表是被const修饰的。也就是说传值捕捉的对象也不能在函数体中进行修改。但是我们使用mutable之后可以修改。
cpp
auto add2 = [=,&b,&c](int x, int y)mutable->int {
a+=10;
b += 10;
c += 10;
return a+b+c;
};
cout << add2(1, 2) << endl;
cout << a << " " << b << " " << c << " " << endl;//10 21 23
但需要注意的是,对于mutable修饰之后的传值捕捉,我们修改的只是拷贝过来的值,对于原来的变量没有影响。
1.3、lambda表达式的实际应用
lambda表达式可以代替仿函数完成一些工作,这个是他很大的一个贡献。因为在lambda之前我们要想写仿函数需要写一个类出来。我们如果不想用仿函数的话用函数指针定义起来也很麻烦。所以说labmda出现之后我们使用的可调用对象定义起来还是很方便的。
cpp
int a[10] = { 1,5,6,4,8,9,1,5,13,12 };
int main()
{
sort(a, a + 10, [](int x, int y) {return x < y;});
for (int i = 0;i < 10;i++)
{
cout << a[i] << " ";
}
return 0;
}
当然除此以外lambda表达式还在其他地方有很多的应用,比如线程初始化,智能指针定制删除器(下下篇会说),资源管理等等。
1.4、lambda的原理
lambda底层其实就是个仿函数对象。我们写一个lambda,编译器底层就会生成一个仿函数的类。对于这个仿函数的类,编译器有几个特定的规则或者说要求:
编译器为每个Lambda生成唯一的类类型
该类包含一个重载的
operator()
Lambda体成为
operator()
的实现捕获的变量成为该类的成员变量
**那么编译器如何确定lambda生成唯一的类类型呢?在vs系列编译器中时通过uuid来实现的。在编译器底层会和每个类名绑定一个uuid,这个uuid时随机的并且重复概率特别特别低,这就保证了每个lambda表达式都是不同的。**其他的编译器有其他的实现方式。但是确定的是需要保证lambda表达式的唯一性。
cpp
auto test1 = [](int x, int y) {return x < y;};
auto test2 = [](int x, int y) {return x < y;};
因为唯一性的确定,对于test1和test2来说虽然他们两个功能相同,但是他们两个的类型时不同的。
2、包装器
2.1、function
function是一个类模板,也是一个包装器。我们可以使用function包装可调用对象,包括函数指针,仿函数,lambda,bind等。如果function为空,此时调用function中的目标会抛出异常。
function底层其实也是个仿函数。因为他也重载了operator()。function包含在头文件functional中。
cpp
#include<iostream>
#include<algorithm>
#include <functional>
using namespace std;
int add(int a, int b) {
return a + b;
}
struct Multiply {
int operator()(int a, int b) const {
return a * b;
}
};
class Math{
public:
int divide(int a, int b) {
return a / b;
}
};
int main()
{
// 返回值 参数类型
function<int(int, int)> func = add;//存储普通对象
cout << func(2, 3); // 输出 5
function<int(int, int)> func1 = Multiply();//存储仿函数
cout << func1(2, 3); // 输出 6
function<int(int, int)> func2 = [](int a, int b) {return a - b;};//存储lambda表达式
cout << func2(5, 3); // 输出 2
Math math;
function<int(Math*, int, int)> func3 = &Math::divide;//存储成员函数
cout << func3(&math,6, 3); // 输出 2
return 0;
}
对于成员函数的包装,我们必须指定类域,并且类域前必须加取地址。而且注意我们在function的尖括号中的参数类型中必须加上隐含的this指针。 并且在调用的时候也要注意传过去一个this指针类型的对象。
但是对于成员函数,在实践中我们更喜欢这样写:
cpp
function<int(Math&&, int, int)> func4 = &Math::divide;//存储成员函数
cout << func4(Math(), 6, 3); // 输出 2
大家可以这样理解,我们最终的目的是调用到成员函数,那么我们可以通过this指针加->去调用成员函数,如果我们传的就是类对象呢,我们也可以通过.*这个操作符来调用到成员函数指针。所以说其实并不是一定传Math*类型的。那么这样的话我们直接传一个临时对象就可以了,就不用特定实例化出来一个对象了。
我们可以使用function和map进行结合,可以让我们的代码看起来更简洁:
传统写法:
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(int i=0;i<tokens.size();i++)
{
if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/")
{
int x=st.top();st.pop();
int y=st.top();st.pop();
switch(tokens[i][0])
{
//一定要注意x和y的顺序
case '+':
st.push(y+x);
break;
case '-':
st.push(y-x);
break;
case '*':
st.push(y*x);
break;
case '/':
st.push(y/x);
break;
}
}
else
{
st.push(stoi(tokens[i]));//要用stoi不然无法处理多位数
}
}
return st.top();
}
};
高效写法:
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
// function作为map的映射可调⽤对象的类型
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y){return x + y;}},
{"-", [](int x, int y){return x - y;}},
{"*", [](int x, int y){return x * y;}},
{"/", [](int x, int y){return x / y;}}
};
for(auto& str : tokens)
{
if(opFuncMap.count(str)) // 操作符
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
int ret = opFuncMap[str](left, right);
st.push(ret);
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
2.2、bind
bind是一个函数模板,他也是一个可调用对象的包装器。 我们可以使用它来调整参数个数和参数顺序。当然我们更常用他来调整参数个数。bind也包含在头文件functional中。
2.2.1、使用bind调整参数顺序
cpp
#include<iostream>
#include<algorithm>
#include <functional>
using namespace std;
//展开命名空间
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int test(int a, int b) {
return a-b;
}
int main()
{
auto test1 = bind(test, _1, _2);
cout << test1(10, 5) << endl;//5
auto test2 = bind(test, _2, _1);
cout << test2(10, 5) << endl;//-5
return 0;
}
std::placeholders
是C++标准库中与std::bind
配合使用的一个命名空间,它提供了一系列占位符(_1
, _2
, _3
等),用于表示绑定函数中将来会被提供的参数位置。我们展开他时可以像上面代码那样展开,也可以直接using namespace placeholders。
其中我们用_1,_2,_3来表示这是传参时的第几个参数,具体的对应关系可以看下图:
2.2.2、使用bind调整参数个数
我们可以使用bind绑死参数,在传参时我们只传没有被绑定的位置:
cpp
auto test3 = bind(test, _1, 10);//绑死第二个参数
cout << test3(10) << endl;//0
cout << test3(10, 1,0,2,4,6,5,1) << endl;//0,第二个参数已经绑死,不管传不传都是10,而且参数可以传任意多个
auto test4 = bind(test,10,_1);
cout << test4(5) << endl;//5
对于test4,具体对应关系如下:

注意一点,我们传两个参数,那么绑定时就写_1,_2,不是说如果原函数第二个参数绑死了我们就传_1,_3。
接下来我们看一下bind和function的结合应用。在上面我们封装成员函数是这样写的:
cpp
function<int(Math&&, int, int)> func4 = &Math::divide;
cout << func4(Math(), 6, 3); // 输出 2
那么其实我们可以使用bind绑死第一个参数,就不用每次都新建一个临时对象了:
cpp
function<int(int, int)> func4 = bind(&Math::divide, Math(), _1, _2);
cout << func4(6, 3) << endl;
当然我们这也可以直接用auto:
cpp
auto func4 = bind(&Math::divide, Math(), _1, _2);
cout << func4(6, 3) << endl;
bind也可以绑死lambda表达式:
cpp
auto lambda = [](int x, int y) { return x * y; };
auto f = std::bind(lambda, _1, 5);
std::cout << f(10) << std::endl; // 输出50
bind也支持嵌套绑定:
cpp
int add(int a, int b) {
return a + b;
}
int main()
{
auto f1 = std::bind(add, std::placeholders::_1, std::placeholders::_2);
auto f2 = std::bind(f1, 10, std::placeholders::_1);
cout<<f2(20); // 输出30
return 0;
}
好了,今天的内容就分享到这,我们下期再见!