目录
[1. lambda的语法](#1. lambda的语法)
[2. 捕捉列表](#2. 捕捉列表)
[3. 应用场景](#3. 应用场景)
[4. lambda的底层原理](#4. lambda的底层原理)
[5. std::function:统一的可调用对象包装器](#5. std::function:统一的可调用对象包装器)
[6. std::bind:参数适配器](#6. std::bind:参数适配器)
1. lambda的语法
lambda本质是一个匿名函数对象,可以定义在函数内部。完整语法:
text
[capture-list] (parameters) -> return-type { function-body }
-
capture-list :捕捉列表,捕捉上下文中需要使用的变量。即使为空也不能省略
[]。 -
parameters:参数列表,参数为空可以连同括号省略。
-
return-type:返回值类型,可以省略让编译器推导。
-
function-body:函数体,不可以省略。
最简单的例子:
cpp
auto add = [](int x, int y) -> int { return x + y; };
auto hello = [] { cout << "hello" << endl; };
lambda之所以用auto接收,是因为它的类型是编译器生成的匿名仿函数类,使用者无法写出具体类型名。
2. 捕捉列表
lambda默认只能使用自己的参数和全局、静态局部变量。想使用外层局部变量,必须捕捉。
-
显式捕捉 :
[x, &y],x值捕捉,y引用捕捉。 -
隐式捕捉 :
[=]全部值捕捉,[&]全部引用捕捉。 -
混合捕捉 :
[=, &x]除x引用外全部值捕捉,[&, x]除x值外全部引用捕捉。混合时第一个必须是=或&,且后续捕捉类型必须相反。
cpp
int a = 0, b = 1, c = 2;
auto f1 = [a, &b] { return a + b; }; // a值捉,b引用捉
auto f2 = [=] { return a + b + c; }; // 全值捉
auto f3 = [&] { a++; c++; }; // 全引用捉
auto f4 = [=, &a] { /* a引用,其他值 */ };
值捕捉默认是const 的,不可修改。要允许修改(但不影响外部变量),加mutable:
cpp
auto f5 = [=]() mutable { a++; return a; }; // OK,修改的是副本
注意,全局变量和静态局部变量不能捕捉,也不需要捕捉,lambda内部直接可用。
3. 应用场景
lambda最大的好处是就地定义可调用对象,比写函数指针或仿函数类轻量得多。排序就是典型场景:
cpp
vector<Goods> v = { {"苹果", 2.1, 5}, {"香蕉", 3, 4}, ... };
// 按价格升序
sort(v.begin(), v.end(),
[](const Goods& g1, const Goods& g2) { return g1._price < g2._price; });
// 按评价降序
sort(v.begin(), v.end(),
[](const Goods& g1, const Goods& g2) { return g1._evaluate > g2._evaluate; });
无需为每种比较规则写一个仿函数类,代码在原地一目了然。
后续在并发编程、智能指针定制删除器等场景中,lambda还会反复出现,适应性很广。
4. lambda的底层原理
编译后不存在什么"lambda语法",编译器会为每个lambda表达式生成一个独一无二的仿函数类。捕捉列表中的变量变成这个类的成员变量,函数体就是operator()的实现。
cpp
auto r = [rate](double money, int year) { return money * rate * year; };
// 等价于:
class __lambda_1 {
double _rate;
public:
__lambda_1(double r) : _rate(r) {}
double operator()(double money, int year) const {
return money * _rate * year;
}
};
__lambda_1 r(rate);
值捕捉的变量默认被const调用,所以不加mutable时不能在lambda体内修改。引用捕捉不存在这个问题,因为它保存的是引用,修改直接影响外部。
5. std::function:统一的可调用对象包装器
在lambda之前,可调用对象有函数指针、仿函数、成员函数指针等,类型各自为政。std::function提供了一个统一的包装器。
cpp
#include <functional>
int f(int a, int b) { return a + b; }
struct Functor {
int operator()(int a, int b) { return a + b; }
};
auto lambda = [](int a, int b) { return a + b; };
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = lambda;
对于成员函数,需要额外处理隐含的this参数:
cpp
class Plus {
public:
double plusd(double a, double b) { return a + b; }
};
function<double(Plus*, double, double)> f4 = &Plus::plusd;
Plus pd;
f4(&pd, 1.1, 1.1);
function<double(Plus, double, double)> f5 = &Plus::plusd;
f5(pd, 1.1, 1.1); // 传对象,内部可能拷贝
function的真正威力在于可以把可调用对象存入容器,实现策略表。比如逆波兰表达式求值:
cpp
map<string, function<int(int, int)>> opMap = {
{"+", [](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; }}
};
int ret = opMap[op](left, right); // 运算符与操作直接映射
新增运算符只需要加一组键值对,不用动核心逻辑。function在这里充当了多态回调的粘合剂。
6. std::bind:参数适配器
bind把一个可调用对象和部分参数绑定,返回一个新的可调用对象。核心用途是调整参数个数和顺序。
cpp
using namespace std::placeholders;
int Sub(int a, int b) { return a - b; }
auto sub1 = bind(Sub, _1, _2); // 等价原函数
auto sub2 = bind(Sub, _2, _1); // 调换参数顺序
auto sub3 = bind(Sub, 100, _1); // 绑死第一个参数为100
auto sub4 = bind(Sub, _1, 100); // 绑死第二个参数为100
sub3(5); // 100 - 5
sub4(5); // 5 - 100
对于成员函数,可以把对象绑死,省去每次都传对象:
cpp
Plus pd;
auto f6 = bind(&Plus::plusd, &pd, _1, _2); // 绑死对象指针
f6(1.1, 2.2);
在实际应用中,bind可以封装出高度特化的函数对象。比如一个计算复利的lambda,通过bind绑死利率和年限,就能得到不同产品的利息计算函数:
cpp
auto calc = [](double rate, double money, int year) -> double {
double ret = money;
for (int i = 0; i < year; ++i)
ret += ret * rate;
return ret - money; // 利息部分
};
auto func_3year_1_5 = bind(calc, 0.015, _1, 3);
cout << func_3year_1_5(1000000) << endl;
bind在C++11中是一个重要的适配器,不过后来lambda本身也足够灵活,很多bind的场景可以直接用lambda替代,代码可读性反而更好。选择哪个,看团队习惯和个人偏好。