前言
随着 C++11 的引入,现代 C++ 语言在语法层面上变得更加灵活、简洁。其中最受欢迎的新特性之一就是 lambda 表达式(Lambda Expression) ,它让我们可以在函数内部直接定义匿名函数 。配合 std::function
包装器 使用,可以大大提高代码的表达力与可维护性。
一、lambda 表达式基础语法
lambda
表达式本质上就是一个匿名函数对象,与普通函数不同的是,它可以定义在函数内部;一般情况下我们是使用
auto
或者模版参数定义的对象去接受lambda
对象。
lambda
表达式本质上是一个可调用对象(函数对象),其语法格式如下:
cpp
[capture](parameter_list) -> return_type {
function_body
};
各部分含义如下:
部分 | 含义 |
---|---|
[] |
捕捉列表(capture list) |
() |
参数列表(与函数类似) |
-> return_type |
返回类型(可省略,自动推导) |
{} |
函数体 |
cpp
int main()
{
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(2, 3); // 输出:5
}
也可以省略 -> int
,由编译器自动推导:
cpp
int main()
{
auto add = [](int a, int b){
return a + b;
};
std::cout << add(2, 3); // 输出:5
}
这里呢,参数列表,如果不需要传参,可以省略,
()
也可以省略;返回值类型可以省略,让编译器自行推导。而参数列表和函数体,就算为空,参数列表的
[]
和函数体的{}
也不能省略。
捕获列表
捕获列表决定了 Lambda 表达式如何访问其所在作用域的变量。
捕获方式 | 语法 | 说明 |
---|---|---|
值捕捉 | [x] |
捕获变量 x 的当前值(拷贝) |
引用捕捉 | [&x] |
捕获变量 x 的引用 |
隐式值捕捉 | [=] |
lambda 使用了哪些变量,编译器就会对哪些变量进行值捕捉 |
隐式引用捕捉 | [&] |
lambda 使用了哪些变量,编译器就会对哪些变量进行引用捕捉 |
混合捕捉 | [=, &y] |
除 y 外的所有变量为值捕获,y 为引用捕获 |
混合捕捉 | [&, x] |
除x 外的所有变量为引用捕捉,x为值捕捉 |
cpp
int main()
{
int x = 10, y = 20;
auto f1 = [=]() { return x + y; }; // 值捕获
auto f2 = [&]() { x += y; }; // 引用捕获,修改外部变量
auto f3 = [x, &y]() { y += x; }; // 混合捕获
return 0;
}
这里lambda
表达式如果在函数局部域中,它可以捕捉lambda
位置之前定义的变量,但是不能捕捉静态局部变量和全局变量(静态局部变量和全局变量也不需要捕捉,lambda
表达式中也可以直接使用)。
如果lambda
定义在全局,那捕捉列表必须为空
值捕获的变量在 Lambda 中是"只读"的,不能修改,除非加上
mutable
。
mutable 关键字
默认情况下,lambda
捕捉列表的值是被const
修饰的,值捕获的变量不能在 Lambda 中被修改 。要想修改值捕获的副本,可以使用 mutable
:
cpp
int main()
{
int a = 5;
auto f = [a]() mutable {
a += 10; // 修改的是a的拷贝,不影响外部 a
cout << a << endl;
};
f(); // 15
std::cout << a << endl; // 5
return 0;
}
lambda 的实际应用场景
与 STL 算法结合
cpp
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(vec.begin(), vec.end(), [](int x) {
std::cout << x << " ";
});
条件查找
cpp
auto it = std::find_if(vec.begin(), vec.end(), [](int x) {
return x > 3;
});
if (it != vec.end()) std::cout << *it; // 输出 4
排序自定义规则
cpp
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // 降序
});
lambda
的原理
lambda
的原理和范围for
非常相似,编译之后从汇编的角度来看,我们就要发现根本就没有lambda
和范围for
;范围for的底层是迭代器,而
lambda
的底层是仿函数对象,简单来说我们写了一个lambda
,编译器就会生成一对应的仿函数。而仿函数的类名是编译器按照一定的规则生产的,保证不同的
lambda
生成的仿函数不同。
lambda的参数/返回值/函数体
就是仿函数operator()
的参数/返回类型/函数体lambda
捕捉列表的本质是生成的仿函数的成员变量。
简单来说就是捕捉列表的量都是lambda
类构造函数的实参;
这样就很好解释值捕捉
和引用捕捉
了:
- 值捕捉:
lambda
生成的仿函数的成员就是对捕捉变量的值拷贝。- 引用捕捉:
lambda
生成的仿函数的成员就是对捕捉变量的引用。
而也支持隐式捕捉,这个就是编译器看需要用哪些对象,就传哪些对象(用多少捕捉多少)。

二、包装器
function
function
是一个类模板,也是一个包装器。std::function
的实例化对象可以包装存储其它可以调用的对象:包括函数指针
、仿函数
、lambda
、bind
表达式;其中存储的对象被称为
std::function
的目标;如果std::function
不含目标,那它为空(调用空目标导致抛出std::bad_function_call
异常。
cpp
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
以上是 function 的原型,他被定义头⽂件中
cpp
#include <functional>
int main()
{
function<int(int, int)> func;
func = [](int a, int b) { return a + b; };
cout << func(3, 4) << endl; // 输出 7
return 0;
}
function
的优势在于统一函数接口、做函数回调或作为参数传递。
函数指针、仿函数、lambda
这些可以调用的对象的类型各不相同,std::function
的优势就在于统一类型;
只要它们返回值,参数都相同,function
就能对它们进行包装;这样在很多的地方就可以声明这些可调用对象的类型;
cpp
int fun(int a, int b)
{
return a + b;
}
struct Fun
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function<int(int, int)> f1 = fun;//函数指针
function<int(int, int)> f2 = Fun();//仿函数
function<int(int, int)> f3 = [](int a, int b) {return a + b; };//lambda
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
//包装静态成员,需要指定类域并且使用&
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
//包装非静态成员函数时
//这里还有一个隐藏的this指针,所以使用function包装后需要传对象或者对象的指针过去才能进行调用
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}
这里再来看一道可以使用function
包装优化的题目
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(auto& e:tokens)
{
if(e == "+"||e == "-" || e == "*" || e == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch(e[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
else
{
st.push(stoi(e));
}
}
return st.top();
}
};
这道题,向上述这样写,特别难受好吧,我们可以使用function
进行优化:
我们知道
+
、-
、*
、/
运算它返回值和参数类型都是相同的,那我们不妨将其包装起来;然后使用
map
存储运算符和对应的函数/调用对象
这样直接使用
map
的[]
就可以访问到要调用的函数/对象。
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>> mp = {
{"+",[](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& e:tokens)
{
if(mp.count(e))//mp中存在就代表是操作符
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(mp[e](left,right));
}
else
st.push(stoi(e));
}
return st.top();
}
};
这样我们代码看起来简洁了好多,用起来也很方便;
如果再多几个运算符,我们只需要在mp
再新增即可。
从这个角度来看:
lambda
算是统一了那些可调用对象的类型,这样对于可调用对象(函数指针
、仿函数
、lambda
),只要参数和返回值相同,那我们就可以使用function
包装起来,方便调用。
bind 与占位符
cpp
simple(1)
template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
with return type (2)
template <class Ret, class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
bind
是一个函数模版,它也是一个可调用对象的包装器;简单来说它就是一个函数适配器,可以对接受的Fn
可调用对象进行处理后返回一个可调用对象。bind
可以用来调整参数个数和参数的顺序。bind
也在这个头文件中。
cpp
int Fun(int a, int b)
{
return (a - b) * 10;
}
int Func(int a, int b, int c)
{
return (a - b - c) * 10;
}
int main()
{
//这里_1始终指接受第一个实参
//_2指接受第二个实参
auto fun1 = bind(Fun, _1, _2);
cout << fun1(10, 5) << endl;//fun1(10,5) -> Fun(10,5)
auto Fun2 = bind(Fun, _2, _1);
cout << Fun2(10, 5) << endl;//fun2(10,5) -> Fun(5,10)
auto fun3 = bind(Fun, 100, _1);
cout << fun3(5) << endl;//fun3(5) -> Fun(100,5)
auto fun4 = bind(Fun, _1, 100);
cout << fun4(5) << endl;//fun4(5) -> Fun(5,100)
auto fun5 = bind(Func, 100, _1, _2);
cout << fun5(5, 1) << endl;//fun5(5,1) -> Func(100,5,1)
auto fun6 = bind(Func, _1, 100, _2);
cout << fun6(5, 1) << endl;//fun6(5,1) -> Func(5,100,1)
auto fun7 = bind(Func, _1, _2, 100);
cout << fun7(5, 1) << endl;//fun7(5,1) -> Func(5,1,100);
return 0;
}
调用
bind
:auto newCallable = bind(callable, arg_list)
(这里newCallable
本身就是一个可调用对象,arg_list
是参数列表,对应给定的callable
的参数(也是可调用对象)这样我们调用
newCallable
,newCallable
就会调用callable
,并传给它arg_list
中的参数。
当我们使用function
去包装类的非静态成员函数时,我们在调用时总是需要传该类型的对象或者该类型对象的指针,来完成调用;
这样的设计好难看,我每一次调用还要创建一个该类型的对象,那我还不如直接去调用呢
cpp
function<double(Plus&&, double, double)> f1 = &Plus::plusd;
Plus pd;
cout << f1(move(pd), 1.1, 1.1) << endl;
cout << f1(Plus(), 1.1, 1.1) << endl;
而
bind
这个绑定,我们可以同来绑定一些固定的参数;就比如这里需要传递该类类型的对象或者该类型对象的指针,我们使用
bind
绑定,直接锁死这个参数,那这样在调用时就不用显示传递了。
cpp
function<double(double, double)> f2 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f2(1.1, 1.1) << endl;
到这里本篇文章内容就结束了
感谢各位的支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws