上一课标准答案:
auto robot = std::make_shared<Robot>();
这里的 robot 不是普通 Robot 对象,而是:
std::shared_ptr<Robot>
也就是一个管理 Robot 对象的智能指针。
-
为什么用:
robot->run();
因为 robot 是智能指针,不是直接的 Robot 对象。
记住:
直接对象 → .
对象的指针 → ->
auto p1 = std::make_shared<Robot>();
auto p2 = p1;
p1 和 p2 管理的是:
同一个 Robot 对象。
不是创建了第二个 Robot。
- 最大区别可以概括成:
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 是智能指针,你怎么解释这句话?