【C++】lambda表达式与std::function/bind包装器

目录

[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替代,代码可读性反而更好。选择哪个,看团队习惯和个人偏好。

相关推荐
树下水月1 小时前
php artisan serve 在window上执行报错的问题
开发语言·php
梦梦代码精1 小时前
电商系统的核心难点:订单与营销系统如何设计?——LikeShop 架构深度拆解(规则计算与状态一致性)
java·开发语言·低代码·架构·开源·github
样例过了就是过了1 小时前
LeetCode热题100 多数元素
c++·算法·leetcode·贪心算法
隐退山林1 小时前
JavaEE进阶:SpringBoot日志
java·开发语言
nbwenren1 小时前
C++ 资源管理 —— RAII
开发语言·c++
棒棒的唐1 小时前
开发中,如何指定不同的php版本启动yii项目
开发语言·php
Shadow(⊙o⊙)1 小时前
进程分析—从操作系统到Linux内核深入
linux·运维·服务器·开发语言·网络·c++·后端
计算机安禾1 小时前
【c++面向对象编程】第6篇:this指针:对象如何知道自己在调用谁?
开发语言·c++
2301_815279521 小时前
如何实现C++ Web 自动化测试实战:常用函数全解析与场景化应用指南
开发语言·前端·c++