C++ 第 28 课:Lambda 表达式

上一课标准答案:

复制代码
auto robot = std::make_shared<Robot>();

这里的 robot 不是普通 Robot 对象,而是:

复制代码
std::shared_ptr<Robot>

也就是一个管理 Robot 对象的智能指针。

  1. 为什么用:

    robot->run();

因为 robot 是智能指针,不是直接的 Robot 对象。

记住:

复制代码
直接对象     → .
对象的指针   → ->
复制代码
auto p1 = std::make_shared<Robot>();
auto p2 = p1;

p1 和 p2 管理的是:

同一个 Robot 对象。

不是创建了第二个 Robot。

  1. 最大区别可以概括成:

Robot* 主要只是保存地址;std::shared_ptr<Robot> 除了能访问对象,还会参与自动管理对象的生命周期。

这就是目前你需要掌握的深度。


第 28 课:Lambda 表达式

从这一课开始,你会明显看到 ROS2 的影子。

以后你可能会遇到:

复制代码
[this](const Msg::SharedPtr msg)
{
    ...
}

第一次看会觉得完全不像我们以前学过的函数。

实际上它只是:

一种可以直接写在当前位置的"小函数"。

这就是 lambda。


1. 为什么需要 lambda?

以前如果我们想写一个函数:

复制代码
void hello()
{
    std::cout << "Hello" << std::endl;
}

调用:

复制代码
hello();

这是正常函数。

但是有些时候,我们只想:

在这里临时写一个很短的功能,而且以后可能根本不需要单独给它取名字。

于是可以写:

复制代码
[]()
{
    std::cout << "Hello" << std::endl;
};

这就是一个 lambda。

你可以暂时把 lambda 理解成:

没有名字的小函数。


2. 第一眼怎么看 lambda?

看最简单版本:

复制代码
[]()
{
    std::cout << "Hello" << std::endl;
}

拆成三部分:

复制代码
[]
()
{}

分别有不同作用。


3. () 很熟悉

以前函数:

复制代码
void hello()

这里:

复制代码
()

放参数。

例如:

复制代码
void printNumber(int x)

lambda 也是一样:

复制代码
[](int x)
{
    std::cout << x;
}

所以:

复制代码
()
↓
参数列表

这一点你已经学过。


4. {} 也很熟悉

复制代码
{
    std::cout << "Hello";
}

就是函数体。

表示:

lambda 被执行时,要做什么。

所以目前只剩最奇怪的:

复制代码
[]

5. [] 是什么?

这个叫:

捕获列表 capture list

先不要急着深究。

最简单:

复制代码
[]

表示:

这个 lambda 暂时不需要使用外面局部变量。

所以:

复制代码
[]()
{
    std::cout << "Hello";
}

可以先读成:

定义一个不使用外部变量、没有参数的小函数,它执行时打印 Hello。


6. 但是只写 lambda,它会自动执行吗?

不会。

例如:

复制代码
[]()
{
    std::cout << "Hello";
};

只是:

创建了一个 lambda。

并没有调用它。

和普通函数一样:

复制代码
void hello()
{
}

你只是定义函数,还没有:

复制代码
hello();

7. 怎么立即执行 lambda?

可以这样:

复制代码
[]()
{
    std::cout << "Hello" << std::endl;
}();

注意最后又多了:

复制代码
()

也就是:

复制代码
[]() { ... }
↑
lambda本身

()
↑
立即调用

所以这段代码会输出:

复制代码
Hello

8. 但实际工程更常见:把 lambda 保存起来

例如:

复制代码
auto hello = []()
{
    std::cout << "Hello" << std::endl;
};

这里:

复制代码
auto hello

表示让编译器自动推断 lambda 的类型。

然后可以:

复制代码
hello();

输出:

复制代码
Hello

9. 为什么需要 auto?

lambda 的真实类型是编译器生成的一种特殊类型。

名字很麻烦,我们通常根本不手写。

所以:

复制代码
auto hello = []()
{
};

非常自然。

你只需要理解:

复制代码
右边
↓
是一个lambda对象

auto
↓
让编译器自己判断它是什么类型

hello
↓
保存这个lambda

以后:

复制代码
hello();

就执行它。


10. lambda 也可以有参数

例如:

复制代码
auto printNumber = [](int x)
{
    std::cout << x << std::endl;
};

调用:

复制代码
printNumber(10);

这里:

复制代码
10
↓
传给 x

所以输出:

复制代码
10

这和普通函数:

复制代码
void printNumber(int x)
{
    std::cout << x;
}

非常像。


11. Lambda 也可以返回数据

例如:

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

调用:

复制代码
int result = add(3, 5);

最终:

复制代码
result = 8

所以 lambda 本质上并没有什么魔法。

它一样有:

复制代码
参数
函数体
返回值

只是写法更紧凑,而且可以直接写在需要的位置。


12. 那 [] 到底什么时候重要?

现在看:

复制代码
int x = 10;

然后:

复制代码
auto print = []()
{
    std::cout << x;
};

这里会有问题。

为什么?

因为:

复制代码
x

是在 lambda 外面定义的局部变量。

而:

复制代码
[]

表示:

我没有捕获外部变量。

所以 lambda 不允许直接使用这个 x。


13. 如果我就是想使用外面的 x 呢?

可以:

复制代码
int x = 10;

auto print = [x]()
{
    std::cout << x << std::endl;
};

这里:

复制代码
[x]

表示:

把外面的 x 捕获进 lambda。

于是:

复制代码
print();

输出:

复制代码
10

14. 但是这里有一个很重要的细节

复制代码
[x]

默认是:

按值捕获

也就是说,可以粗略理解为 lambda 得到了一份 x 的副本。

例如:

复制代码
int x = 10;

auto print = [x]()
{
    std::cout << x << std::endl;
};

x = 20;

print();

通常输出:

复制代码
10

为什么?

因为创建 lambda 时:

复制代码
外面的 x = 10
↓
复制一份给 lambda

后来外面的:

复制代码
x = 20;

并没有改变 lambda 已经保存的那份 10。


15. 如果希望 lambda 操作原来的 x 呢?

使用引用捕获:

复制代码
int x = 10;

auto change = [&x]()
{
    x = 100;
};

change();

最后:

复制代码
x = 100

因为:

复制代码
[&x]

表示:

通过引用使用外面的 x。

这和你之前学的:

复制代码
int& a

思想是一样的。


16. 所以 [x] 和 [&x]

非常重要:

复制代码
[x]
↓
按值捕获
↓
得到x的一份副本

而:

复制代码
[&x]
↓
按引用捕获
↓
直接操作外面的x

这就是你前面为什么必须先学:

复制代码
值传递
引用

现在这些知识开始回来了。


17. 还有 [=]

以后你可能看到:

复制代码
[=]()
{
}

表示:

lambda 使用到的外部变量,默认按值捕获。

例如:

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

auto f = [=]()
{
    std::cout << a + b;
};

可以使用 a 和 b。


18. 还有 [&]

复制代码
[&]()
{
}

表示:

使用到的外部变量默认按引用捕获。

例如:

复制代码
int a = 10;

auto f = [&]()
{
    a = 100;
};

调用后外面的:

复制代码
a = 100

19. 但 ROS2 里你最先要理解的是 [this]

终于来到最重要的地方。

假设:

复制代码
class Robot
{
public:
    void start()
    {
        auto f = [this]()
        {
            running = true;
        };

        f();
    }

private:
    bool running = false;
};

这里:

复制代码
[this]

表示:

把当前对象的 this 指针捕获进 lambda。


20. 为什么 lambda 需要 this?

你前面已经知道:

复制代码
this

指向:

当前正在执行成员函数的对象。

假设:

复制代码
Robot robot;
robot.start();

进入:

复制代码
start()

时:

复制代码
this → robot

而 lambda 如果想访问:

复制代码
running

这个对象内部的成员变量,就需要能够找到当前对象。

所以使用:

复制代码
[this]

21. [this] 可以怎么理解?

非常适合先记成:

允许这个 lambda 使用当前对象的成员。

例如:

复制代码
class Motor
{
public:
    void test()
    {
        auto f = [this]()
        {
            speed = 100.0;
        };

        f();
    }

private:
    double speed = 0.0;
};

lambda 里面:

复制代码
speed = 100.0;

实际上是在修改:

复制代码
this->speed

也就是当前 Motor 对象自己的 speed。


22. 这为什么和 ROS2 特别相关?

因为 ROS2 是一个非常典型的:

事件驱动系统。

什么意思?

不是所有代码都按照:

复制代码
第一行
第二行
第三行
第四行

一直顺序执行。

而是经常:

复制代码
消息什么时候来了
↓
就自动执行某个函数

这就叫:

回调 callback


23. 先不要急着学 ROS2 API

我们先做一个假想例子。

假设:

复制代码
onMessage(
    [](int data)
    {
        std::cout << data;
    }
);

这里相当于告诉程序:

将来有消息来的时候,请执行这个 lambda。

lambda 本身就是:

复制代码
[](int data)
{
    std::cout << data;
}

24. 这就是"回调函数"的核心思想

你可以把它想成外卖电话。

你告诉外卖员:

"到了以后打我电话。"

你不是现在立刻接电话。

而是:

复制代码
现在
↓
把联系方式交出去

将来条件满足
↓
对方主动调用

在程序里:

复制代码
现在
↓
把函数/lambda交给系统

将来消息到达
↓
系统自动调用这个函数

这就是 callback。


25. ROS2 订阅消息就是这种思想

例如你以后订阅机械臂状态:

复制代码
/joint_states

逻辑大致是:

复制代码
创建订阅器
↓
告诉ROS2:
"有JointState消息时执行这个函数"
↓
ROS2等待
↓
消息到达
↓
自动调用回调

所以 lambda 特别适合写回调。


26. 看一个简化版 ROS2 风格代码

先不要求你知道 ROS2 API:

复制代码
[this](const Msg::SharedPtr msg)
{
    current_angle = msg->angle;
}

我们现在已经能拆很多东西了。


第一部分

复制代码
[this]

表示:

lambda 可以使用当前对象。


第二部分

复制代码
(const Msg::SharedPtr msg)

这是参数。

表示:

消息来了以后,ROS2 会把收到的消息传进来,变量名叫 msg。


第三部分

复制代码
msg->angle

为什么是 ->?

因为 msg 是类似智能指针的东西。

所以:

复制代码
msg->angle

表示:

访问收到消息对象里的 angle。


第四部分

复制代码
current_angle = msg->angle;

表示:

把消息里的角度保存到当前对象的 current_angle。


27. 你现在已经能看懂这句的骨架了

复制代码
[this](const Msg::SharedPtr msg)
{
    current_angle = msg->angle;
}

翻译成人话:

当这个 lambda 被调用时,它会收到一个消息对象;它可以访问当前类对象,并把消息中的 angle 保存到当前对象自己的 current_angle 中。

这其实已经非常接近真正 ROS2 subscriber callback 了。


28. 放进 VLA 场景理解

以后你可能有:

复制代码
相机
↓
ROS2图像Topic
↓
VLA节点
↓
模型推理
↓
Action
↓
机械臂控制节点

假设控制节点收到 VLA 输出:

复制代码
目标关节角:
[10, 20, 30, 40, 50, 60]

就可以有一个回调:

复制代码
[this](const ActionMsg::SharedPtr msg)
{
    target_joint_1 = msg->joint1;
}

意思就是:

VLA action 消息一到,自动读取消息并更新控制目标。

所以 lambda 并不是为了"写漂亮 C++"。

它是直接服务于:

ROS2 回调式编程。


29. 这一课你先别过早钻复杂捕获规则

现在只需要真正掌握这四种:

复制代码
[]

不捕获外部变量。

复制代码
[x]

按值捕获 x。

复制代码
[&x]

按引用捕获 x。

复制代码
[this]

捕获当前对象,允许 lambda 使用对象成员。

尤其最后一个:

复制代码
[this]

对 ROS2 非常重要。


30. 本课核心表

写法 现在怎么理解
[](){} 一个简单 lambda
[](int x){} 接收一个参数
[x](){} 使用外部 x 的副本
[&x](){} 通过引用使用外部 x
[this](){} 使用当前对象的成员
auto f = ... 把 lambda 保存起来
f() 执行 lambda

本课小练习

第 1 题

复制代码
auto f = []()
{
    std::cout << "Hi";
};

这里只定义了 lambda,还是已经执行了?


第 2 题

复制代码
int x = 10;

auto f = [x]()
{
    std::cout << x;
};

x = 20;

f();

输出通常是多少?


第 3 题

复制代码
int x = 10;

auto f = [&x]()
{
    x = 50;
};

f();

最后外面的 x 是多少?


第 4 题

在类成员函数里面:

复制代码
[this]()
{
    speed = 100;
}

这里 [this] 的作用是什么?


第 5 题

看到:

复制代码
msg->data

如果 msg 是智能指针,你怎么解释这句话?

相关推荐
代码什么用1 小时前
LeetCode-560.和为 K 的子数组
算法·leetcode
Frank_refuel1 小时前
深入理解RpcRouter(远端调用模块)
开发语言·qt
All for pursuit.1 小时前
【回溯-3】39.组合总和
数据结构·c++·算法·leetcode
黑科技iOS上架1 小时前
双引擎解决oc/swift开发引用加固难题
开发语言·ios·swift·审核·ios混淆·执行程序差异化
释厄6231 小时前
BSD 基础论——不要忘记连接·不要停滞循环·AI基础算法
人工智能·算法
yujunl1 小时前
U9客开中UI插件开发过程中碰到有一个坑
开发语言
见叶之秋1 小时前
【C++】一条双向链的诞生:手写 list 模拟实现
c++
沫璃染墨1 小时前
《从零入门Linux系统篇(五十六):线程篇·九——生产者消费者模型:从条件变量到BlockingQueue实现》
linux·服务器·c++·驱动开发·设计模式·架构·系统架构
iCxhust1 小时前
C语言struct结构体实现类定义
c语言·开发语言·单片机·嵌入式硬件·51单片机