C++ std::function
与 std::bind
入门详解
在 C++11 之后,std::function
和 std::bind
成为现代 C++中实现灵活回调和高可扩展性代码的"神器"。本节先介绍它们的基础用法,再结合 ROS 2 节点开发,讲解如何通过这两者实现灵活的订阅者设计。
1. std::function
是什么?
std::function
是 C++11 引入的一个通用函数封装器。简单来说,它可以保存任何可以"像函数一样被调用"的东西,比如:
- 普通函数
- lambda 表达式
- 成员函数指针(需要结合
std::bind
) - 其他可调用对象(如仿函数)
为什么要用 std::function
?
std::function
让你的接口"更通用",可以统一存储和调用不同类型的回调。例如你写一个库,用户可以用普通函数、lambda 表达式、成员函数等各种方式传入回调,只要签名匹配都能用。
常见用法举例
包装普通函数
cpp
#include <functional>
#include <iostream>
void foo(int x) {
std::cout << "foo: " << x << std::endl;
}
int main() {
std::function<void(int)> f = foo;
f(42); // 输出: foo: 42
}
包装 lambda 表达式
cpp
std::function<void(int)> f = [](int x) {
std::cout << "Lambda: " << x << std::endl;
};
f(100); // 输出: Lambda: 100
包装成员函数(需要 std::bind
,见下节)
2. std::bind
是什么?
std::bind
也是 C++11 引入的,用于把函数和它的部分参数"提前绑定"起来,生成一个新的可调用对象,常见于成员函数绑定。
为什么需要 std::bind
?
成员函数指针和普通函数指针不同,它需要知道绑定到哪个对象上(需要一个 this 指针)。直接赋值到 std::function
里会出错。
举例:绑定成员函数
cpp
class MyClass {
public:
void print(int x) {
std::cout << "MyClass: " << x << std::endl;
}
};
MyClass obj;
std::function<void(int)> f = std::bind(&MyClass::print, &obj, std::placeholders::_1);
f(7); // 输出: MyClass: 7
- &MyClass::print:类的成员函数指针
- &obj:绑定成员函数到哪个对象
- std::placeholders::_1:占位符,表示调用 f 时传入的第一个参数
ROS 2 C++ 节点封装与灵活回调订阅者设计实践
在 ROS 2 C++ 项目开发中,节点(Node)是功能的最小单元。为了提升开发效率和代码复用性,很多团队会对官方的 rclcpp::Node
做二次封装,形成统一的节点基类,并对发布、订阅、服务等接口进行统一管理和增强。
下面一个"通用节点类"为例,介绍如何通过 std::function
和 std::bind
让订阅接口支持多种回调风格,极大简化回调注册和使用。
1. 设计目标
- 统一接口:对发布、订阅、服务等常用操作做统一封装,便于调用
- 灵活回调:订阅者支持普通函数、lambda 表达式、成员函数等多种回调写法
- 易于扩展:后续功能增强和维护更方便
2. 通用节点基类结构
节点基类继承自 rclcpp::Node
,内部自带回调分组(CallbackGroup),并提供统一的发布、订阅等接口。示例代码如下:
cpp
class MyNode: public rclcpp::Node {
public:
template<typename MessageType>
typename rclcpp::Publisher<MessageType>::SharedPtr CreatePublisher(
const std::string& topic, const rclcpp::QoS& qos) {
if (topic.empty()) {
throw std::invalid_argument("topic is empty");
}
return this->create_publisher<MessageType>(topic, qos);
}
template<typename MessageType>
typename rclcpp::Subscription<MessageType>::SharedPtr CreateSubscriber(
const std::string& topic, const rclcpp::QoS& qos,
std::function<void(std::unique_ptr<MessageType>)> callback) {
if (topic.empty()) {
throw std::invalid_argument("topic is empty");
}
rclcpp::SubscriptionOptions options;
options.callback_group = cb_group_;
auto wrapper_cb = [=](typename MessageType::UniquePtr msg) {
// ... 可加监控/异常处理等逻辑
callback(std::move(msg));
// ...
};
return create_subscription<MessageType>(topic, qos, wrapper_cb, options);
}
// ... 其他接口如 CreateServer, CreateClient ...
private:
std::shared_ptr<rclcpp::CallbackGroup> cb_group_;
// 创建互斥型回调组(同一时刻只允许一个回调执行)
explicit MyNode(const std::string& node_name, const NodeOptions& opt = NodeOptions())
: Node(node_name, opt),
cb_group_(create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive)) {}
};
3. 支持多种回调写法
得益于 std::function
,CreateSubscriber
支持如下多种回调风格:
-
普通函数
cppvoid MyCallback(std::unique_ptr<MyMsg> msg) { /* ... */ } node->CreateSubscriber<MyMsg>("topic", qos, MyCallback);
-
lambda 表达式
cppnode->CreateSubscriber<MyMsg>("topic", qos, [](std::unique_ptr<MyMsg> msg) { /* ... */ });
-
成员函数(结合 std::bind)
cppclass MyClass { public: void Proc() { node_->CreateSubscriber<MyMsg>("topic", qos, std::bind(&MyClass::HandleMsg, this, std::placeholders::_1)); } private: void HandleMsg(std::unique_ptr<MyMsg> msg) { /* ... */ } std::shared_ptr<MyNode> node_; };
只要回调的参数类型和签名匹配,用户可以自由选择最适合自己的写法。
4. 总结
- 节点基类封装能极大提升代码复用性和一致性。
- 利用
std::function
,让订阅接口兼容多种回调写法,提升了灵活性和易用性。