C++11——Lambda、function、bind 与可调用对象

今天介绍一些在实际 C++ 编程中非常常用的工具:

Lambda、std::function、bind。

同时结合前面介绍的可变参数模板和完美转发,可以看到 C++11 是怎样一步一步增强泛型编程能力的。


一、Lambda 是什么?

如果以前想定义一个简单的函数对象,通常有两种办法:

函数指针

cpp 复制代码
int (*func)(int, int);

仿函数

自己定义一个类:

cpp 复制代码
struct Add
{
    int operator()(int a, int b)
    {
        return a + b;
    }
};

这些方法都可以,但是写起来比较麻烦。

C++11 提供了:

Lambda 表达式

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。


二、Lambda 的基本语法

基本形式:

cpp 复制代码
[capture-list](parameters) -> return_type
{
    function_body
};

最简单的例子:

cpp 复制代码
auto add = [](int x, int y)
{
    return x + y;
};

然后:

cpp 复制代码
cout << add(1, 2);

就可以得到:

text 复制代码
3

三、为什么 Lambda 的类型不好写?

Lambda 本质上会生成一个独特的匿名函数对象类型。

但是这个类型的名字不是我们自己定义的。

所以实际开发中通常使用:

cpp 复制代码
auto

来接收:

cpp 复制代码
auto func = []()
{
    cout << "hello";
};

而不是尝试写出它真正的类型名称。


四、捕捉列表

lambda 表达式中默认只能用 lambda 函数体和

参数中的变量,如果想用外层作用域中的变量

就需要进行捕捉

例如:

cpp 复制代码
int a = 10;
int b = 20;

auto func = [a, &b]()
{
    cout << a << endl;
    cout << b << endl;
};

这里:

text 复制代码
[a, &b]

就是:捕捉列表。


五、值捕捉和引用捕捉

例如:

cpp 复制代码
[a, &b]

表示:

text 复制代码
a
→ 值捕捉

b
→ 引用捕捉

值捕捉本质上可以理解成:

把外部对象的一份值保存下来。

而引用捕捉则是:

让 Lambda 继续引用外部对象。


六、隐式捕捉

除了:

cpp 复制代码
[a, &b]

还可以:

cpp 复制代码
[=]

表示:

需要使用的外部变量都采用值捕捉。

或者:

cpp 复制代码
[&]

表示:

需要使用的外部变量都采用引用捕捉。

例如:

cpp 复制代码
auto func = [&]()
{
    a++;
    b++;
    c++;
};

这样写起来会非常方便。


七、mutable

默认情况下,Lambda 的值捕捉具有 const 属性。

如果我们希望修改值捕捉得到的那一份副本,可以使用:

cpp 复制代码
[=]() mutable
{
    ...
};

这里修改的是:

Lambda 内部自己的那份副本。

并不会修改外面的原始变量。


八、Lambda 最实用的场景:STL 算法

例如现在有一个商品结构:

cpp 复制代码
struct Goods
{
    string _name;
    double _price;
    int _evaluate;
};

如果想按照价格排序,以前可能需要专门写:

cpp 复制代码
struct ComparePriceLess
{
    bool operator()(const Goods& g1,
                    const Goods& g2)
    {
        return g1._price < g2._price;
    }
};

现在直接:

cpp 复制代码
sort(v.begin(), v.end(),
     [](const Goods& g1,
        const Goods& g2)
     {
         return g1._price < g2._price;
     });

就可以了。

如果下一次想按照评价排序:

cpp 复制代码
sort(v.begin(), v.end(),
     [](const Goods& g1,
        const Goods& g2)
     {
         return g1._evaluate > g2._evaluate;
     });

这就是 Lambda 非常实用的地方:

需要一个临时的可调用对象时,不需要专门定义一个类。


九、Lambda 的底层原理

Lambda 看起来像一个特殊语法,但实际上:

编译器最终会把 Lambda 转化成一个类似仿函数的类。

例如:

cpp 复制代码
double rate = 0.49;

auto r = [rate](double money, int year)
{
    return money * rate * year;
};

本质上可以近似理解成:

cpp 复制代码
class Lambda
{
public:
    Lambda(double rate)
        : _rate(rate)
    {}

    double operator()(double money, int year)
    {
        return money * _rate * year;
    }

private:
    double _rate;
};

也就是说:

text 复制代码
Lambda捕捉的数据
↓
变成类成员变量

Lambda函数体
↓
变成 operator()

这也是为什么我们可以说:

Lambda 本质上仍然是一个函数对象。


十、std::function

有了 Lambda 后,又出现了一个新的问题:

不同的可调用对象,类型都不一样。

例如:

text 复制代码
函数指针
仿函数
Lambda
bind返回对象

这些类型本身很难统一。

C++11 提供:

cpp 复制代码
std::function

std::function 是一个可调用对象包装器,可以统一包装函数指针、仿函数、Lambda、bind 表达式等。


十一、为什么 function 很方便?

例如:

cpp 复制代码
function<int(int, int)> f;

这个类型表示:

接收两个 int,返回一个 int 的可调用对象。

于是:

cpp 复制代码
int Add(int a, int b)
{
    return a + b;
}

可以放进去:

cpp 复制代码
function<int(int, int)> f = Add;

Lambda 也可以:

cpp 复制代码
f = [](int a, int b)
{
    return a + b;
};

仿函数同样可以。

所以:

text 复制代码
不同类型的可调用对象
        ↓
std::function
        ↓
统一类型

这对于构造:

text 复制代码
回调
函数表
操作符映射
事件系统

等结构非常方便。


十二、bind

除了:

cpp 复制代码
lambda
function

C++11 还提供:

cpp 复制代码
std::bind

函数适配器。

它可以把一个已有的可调用对象包装成另一个新的可调用对象,同时:

  • 调整参数顺序
  • 固定部分参数
  • 改变调用方式

十三、bind 最简单的例子

假设:

cpp 复制代码
int Sub(int a, int b)
{
    return (a - b) * 10;
}

可以:

cpp 复制代码
auto sub1 = bind(Sub, _1, _2);

那么:

cpp 复制代码
sub1(10, 5);

实际就是:

cpp 复制代码
Sub(10, 5);

十四、调整参数顺序

甚至可以:

cpp 复制代码
auto sub2 = bind(Sub, _2, _1);

然后:

cpp 复制代码
sub2(10, 5);

实际调用:

cpp 复制代码
Sub(5, 10);

所以 _1 表示:

新函数的第一个参数。

_2 表示:

新函数的第二个参数。


十五、bind 还可以固定参数

例如:

cpp 复制代码
auto sub3 = bind(Sub, 100, _1);

那么:

cpp 复制代码
sub3(5);

实际上执行:

cpp 复制代码
Sub(100, 5);

也就是说:

text 复制代码
原函数
↓
bind固定部分参数
↓
生成一个新的可调用对象

这在需要大量"半固定参数的函数"时会比较方便。

cpp 复制代码
#include <functional>
#include <iostream>

int add(int a, int b) {
    return a + b;
}

int main() {
    using namespace std::placeholders;

    // 把 add 的第一个参数固定为 10,第二个参数留给调用者
    auto f1 = std::bind(add, 10, _1);
    std::cout << f1(5) << '\n';   // 输出 15,相当于 add(10, 5)

    // 全部固定,调用时不需要参数
    auto f2 = std::bind(add, 1, 2);
    std::cout << f2() << '\n';    // 输出 3

    // 参数重排:_2 作为第一个参数,_1 作为第二个参数
    auto f3 = std::bind(add, _2, _1);
    std::cout << f3(3, 4) << '\n'; // 输出 7,相当于 add(4, 3)

    // 同一个参数使用多次
    auto f4 = std::bind(add, _1, _1);
    std::cout << f4(7) << '\n';   // 输出 14,相当于 add(7, 7)
}

相关推荐
优化Henry1 小时前
LTE站点以太网光模块告警处理实践
运维·网络·笔记·学习·信息与通信
白杨尚青1 小时前
C++入门篇(十一):string(中)——容量与增删改查:一篇吃透所有常用接口(万字详解)
java·开发语言·c++
职豚求职小程序1 小时前
27届携程笔试测评|能力测评+英语测评高分过
笔记
朔北之忘 Clancy2 小时前
2024 CSP-J 第二轮真题解析
c++·算法·青少年编程·noip·csp·信奥赛·noi
Lazionr2 小时前
set和map的使用——从基础接口到实际应用
c++
重生之小比特2 小时前
【C++进阶】哈希表实现
开发语言·c++·哈希
鬼手点金2 小时前
Claude Code示范案例-进阶学习路径
java·服务器·前端·学习·计算机视觉·前向传播
有范先生2 小时前
常温即食米饭和自热米饭有什么区别,户外出行该怎么选
笔记
迪丽热爱2 小时前
多媒体应用24-907(补)
学习