# cpp_pubsub — C++ Topic 发布订阅 > 跟 `py_pubsub` 一样的东西,用 C++ 写。**只用 Python 的项目可以跳过这个**。 > > 预计学习时间:1-2 小时。 > > **前置知识**:读懂 py_pubsub 的 README + 基础 C++(知道 class / shared_ptr / std:: 是什么)。 --- ## 这是什么? `py_pubsub` 的 C++ 版本。**同一个 Topic,Python 和 C++ 节点互通**(都用 `std_msgs/String` 类型)。 **本包存在的意义**: - 看到 Python 节点发的消息,C++ 节点能收到(反之亦然) - 学会用 **rclcpp**(ROS2 C++ 客户端库) 写节点 - 知道工业级 ROS2 项目的 C++ 代码长什么样 --- ## 🤔 我已经会 py_pubsub 了,为什么还要学 C++ 版? **实话**:Python 写 ROS2 简单 90%,**大多数项目用 Python 就够了**。但有些场景用 C++ 更合适: | 场景 | 为什么可能用 C++ | |---|---| | 性能敏感(图像/点云/SLAM) | Python GC 不可控 + 速度慢 | | 已有 C++ 库要复用 | OpenCV、PCL、MoveIt2 全是 C++ | | 嵌入式部署 | 部分芯片只支持 C/C++ | **如果你的项目不涉及上面这些,只用 Python 就行,不必学这个包。** --- ## 🎯 学完之后你能做什么? 1. ✅ 用 C++ 写 ROS2 节点(知道 `rclcpp::Node` 是什么) 2. ✅ 理解 Python ↔ C++ 跨语言互通(为什么能行?) 3. ✅ 知道 `shared_ptr` / `std::bind` / `rclcpp::QoS` 这些 C++ 套路 4. ✅ 读懂 ROS2 官方 C++ 示例代码 --- ## 📁 文件结构 ``` src/cpp_pubsub/ ├── src/ │ ├── chatter_publisher.cpp/hpp # Publisher 类(声明 + 实现) │ ├── chatter_publisher_main.cpp # Publisher 的 main 入口(单独文件) │ ├── chatter_subscriber.cpp/hpp # Subscriber 类(声明 + 实现) │ └── chatter_subscriber_main.cpp # Subscriber 的 main 入口 ├── launch/pubsub_cpp_launch.py # 启动两个 cpp 节点 ├── test/test_pub_sub.cpp # gtest 单元测试 ├── CMakeLists.txt # C++ 编译配置 └── package.xml # ROS2 包元数据 ``` **C++ 项目比 Python 多一个东西**:`CMakeLists.txt`(告诉编译器怎么编译)。 --- ## 🚀 跑起来 ```bash source /opt/ros/humble/setup.bash source /root/ros2_ws/install/setup.bash # 只跑 C++ 的两个节点 ros2 launch cpp_pubsub pubsub_cpp_launch.py ``` **预期输出**: ``` [INFO] [chatter_publisher_cpp]: Publishing: "Hello World C++: 0" [INFO] [chatter_publisher_cpp]: Publishing: "Hello World C++: 1" ... [INFO] [chatter_subscriber_cpp]: I heard: Hello World C++: 0 ``` **跨语言验证**(另开终端,跟 Python 互通): ```bash # 终端 1: C++ Publisher ros2 run cpp_pubsub chatter_publisher_cpp # 终端 2: Python Subscriber(可以!因为都用 std_msgs/String) ros2 run py_pubsub chatter_subscriber ``` **预期效果**:Python Subscriber 能收到 C++ Publisher 的消息(和反之)。 --- ## 📖 C++ 黑魔法词典(新手必看) 读代码前先扫一眼这些符号: | 符号 | 含义 | Python 对应 | |---|---|---| | `class Foo : public rclcpp::Node` | Foo 继承 rclcpp::Node | `class Foo(Node):` | | `rclcpp::Node` | ROS2 C++ 节点基类 | `rclpy.node.Node` | | `std::shared_ptr` | 共享指针(自动内存管理) | 不用写,GC 自动管 | | `::SharedPtr` | rclcpp 的共享指针类型 | 无 | | `std::make_shared(...)` | 创建 shared_ptr 的工厂函数 | 无 | | `std::bind(&Foo::method, this)` | 把方法 + this 绑成一个可调用对象 | lambda 即可 | | `std::placeholders::_1` | bind 的占位符,代表"第一个参数" | lambda 参数 | | `std::chrono::seconds(1)` | 1 秒的 std 写法(可简写为 `1s`) | `1.0` | | `create_wall_timer(...)` | 创建定时器 | `create_timer(...)` | | `create_publisher(topic, qos)` | 模板参数 T 是消息类型 | `create_publisher(T, topic, qos)` | | `` | 嵌套命名空间,ROS 消息类型 | `String`(从 std_msgs.msg 导入) | **`std::bind` 是 C++ 的"高阶函数"**,把方法 + 对象绑在一起,让定时器能回调。Python 里 lambda 就能搞定。 --- ## 📖 Publisher 完整代码 ```cpp // chatter_publisher.hpp - 类声明 #pragma once #include #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" namespace cpp_pubsub { class ChatterPublisher : public rclcpp::Node { public: explicit ChatterPublisher(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()); private: void timer_callback(); // shared_ptr:节点销毁时自动释放这些对象(不用手动 delete) rclcpp::Publisher::SharedPtr publisher_; rclcpp::TimerBase::SharedPtr timer_; std::size_t count_; }; } // namespace cpp_pubsub // chatter_publisher.cpp - 类实现 #include "cpp_pubsub/chatter_publisher.hpp" #include #include namespace cpp_pubsub { ChatterPublisher::ChatterPublisher(const rclcpp::NodeOptions & options) : rclcpp::Node("chatter_publisher_cpp", options), // 调父类构造函数,节点名 "chatter_publisher_cpp" count_(0) { // 1) 声明参数(C++ 用 ParameterDescriptor + set__description) // ⚠️ 注意:是 set__description(双下划线),不是 set_description // 双下划线 = ROS2 自动生成的 setter(类似 Python 的 description 属性) this->declare_parameter( "message_prefix", "Hello World C++: ", rcl_interfaces::msg::ParameterDescriptor().set__description("消息前缀")); this->declare_parameter( "publish_rate_hz", 1.0, rcl_interfaces::msg::ParameterDescriptor().set__description("发布频率 (Hz)")); // 2) 读参数 const std::string prefix = this->get_parameter("message_prefix").as_string(); const double rate = this->get_parameter("publish_rate_hz").as_double(); // 3) 创建 Publisher // 模板参数 表示消息类型 // 第二个参数 10 是队列大小(跟 Python 一样) publisher_ = this->create_publisher("chatter_cpp", 10); // 4) 创建定时器 // std::bind 把方法 + this 绑成一个"函数对象" // 让定时器能调用 ChatterPublisher::timer_callback timer_ = this->create_wall_timer( std::chrono::milliseconds(static_cast(1000.0 / rate)), std::bind(&ChatterPublisher::timer_callback, this)); } void ChatterPublisher::timer_callback() { auto msg = std_msgs::msg::String(); msg.data = "Hello World C++: " + std::to_string(count_++); publisher_->publish(msg); // 发布! } } // namespace cpp_pubsub ``` **`set__description` 双下划线**:这是 ROS2 的 IDL(接口描述语言)生成器自动生成的 setter 命名约定。**返回自身引用,可以链式调用**: ```cpp ParameterDescriptor().set__description("...") // 链式风格 // 等价于 auto d = ParameterDescriptor(); d.set__description("..."); ``` --- ## 📖 Subscriber 完整代码 ```cpp // chatter_subscriber.hpp #pragma once #include #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" namespace cpp_pubsub { class ChatterSubscriber : public rclcpp::Node { public: explicit ChatterSubscriber(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()); private: void message_callback(const std_msgs::msg::String::SharedPtr msg); rclcpp::Subscription::SharedPtr subscription_; std::size_t received_count_; }; } // namespace cpp_pubsub // chatter_subscriber.cpp #include "cpp_pubsub/chatter_subscriber.hpp" #include namespace cpp_pubsub { ChatterSubscriber::ChatterSubscriber(const rclcpp::NodeOptions & options) : rclcpp::Node("chatter_subscriber_cpp", options), received_count_(0) { this->declare_parameter( "topic_name", "chatter", rcl_interfaces::msg::ParameterDescriptor().set__description("订阅话题名")); const std::string topic_name = this->get_parameter("topic_name").as_string(); // create_subscription<消息类型>(话题名, 队列大小, callback) // _1 是 std::bind 的占位符,代表"收到的 msg"(传给 message_callback 的第一个参数) subscription_ = this->create_subscription( topic_name, 10, std::bind(&ChatterSubscriber::message_callback, this, std::placeholders::_1)); } void ChatterSubscriber::message_callback(const std_msgs::msg::String::SharedPtr msg) { received_count_++; if (received_count_ % 10 == 0) { RCLCPP_INFO(this->get_logger(), "recv #%zu: \"%s\"", received_count_, msg->data.c_str()); } } } // namespace cpp_pubsub ``` **对比 Python Subscriber**: ```python self.subscription = self.create_subscription( String, 'chatter', self.listener_callback, 10) def listener_callback(self, msg): self.get_logger().info(f'I heard: {msg.data}') ``` **主要区别**: - C++ 用 `std::bind` + `std::placeholders::_1` 占位符 - C++ 消息是 `SharedPtr`(智能指针) - C++ 字符串访问用 `msg->data.c_str()`(指针解引用 + C 字符串转换) --- ## 📖 main 函数(单独文件) ```cpp // chatter_publisher_main.cpp int main(int argc, char * argv[]) { rclcpp::init(argc, argv); // 初始化 ROS2 rclcpp::spin(std::make_shared()); // 创建节点 + spin rclcpp::shutdown(); // 关闭 ROS2 return 0; } ``` **为什么 main 单独一个文件**:类实现要单独编译给测试用(`gtest` 链接时会出冲突,如果 main 在类实现里)。 **`spin` 是什么**:死循环,反复调用所有 callback(定时器、订阅者、Service...)。按 `Ctrl+C` 退出。 --- ## 📖 CMakeLists.txt 怎么读? ```cmake cmake_minimum_required(VERSION 3.16) project(cpp_pubsub LANGUAGES CXX) # 1) 找依赖 find_package(ament_cmake REQUIRED) # ROS2 构建系统 find_package(rclcpp REQUIRED) # ROS2 C++ 客户端 find_package(std_msgs REQUIRED) # 标准消息 find_package(ament_cmake_gtest REQUIRED) # gtest 支持(test_depend) # 2) 类实现编译成共享库(给 gtest 用) add_library(${PROJECT_NAME}_core SHARED src/chatter_publisher.cpp src/chatter_subscriber.cpp) target_include_directories(${PROJECT_NAME}_core PUBLIC $ $) ament_target_dependencies(${PROJECT_NAME}_core rclcpp std_msgs) # 3) 可执行文件(链接共享库 + main 单独文件) add_executable(chatter_publisher_cpp src/chatter_publisher_main.cpp) target_link_libraries(chatter_publisher_cpp ${PROJECT_NAME}_core) add_executable(chatter_subscriber_cpp src/chatter_subscriber_main.cpp) target_link_libraries(chatter_subscriber_cpp ${PROJECT_NAME}_core) # 4) 安装 + 测试 install(TARGETS chatter_publisher_cpp chatter_subscriber_cpp ${PROJECT_NAME}_core DESTINATION lib/${PROJECT_NAME}) if(BUILD_TESTING) ament_add_gtest(test_pub_sub test/test_pub_sub.cpp) target_link_libraries(test_pub_sub ${PROJECT_NAME}_core) endif() ament_package() ``` **跟 Python setup.py 对比**:CMakeLists.txt 更复杂,但更精确控制编译(链接、include 路径、依赖)。 --- ## 🧪 跑测试 ```bash colcon test --packages-select cpp_pubsub colcon test-result --all --verbose ``` **预期**:`cpp_pubsub: gtest 3/3 ✓` 全部通过。 --- ## 🔧 自己改代码 1. 改 `src/chatter_publisher.cpp`(比如改默认消息前缀) 2. **必须重编译**(C++ 不会自动生效): ```bash colcon build --packages-select cpp_pubsub ``` 3. 重跑 launch(旧的先 `Ctrl+C` 停掉) --- ## 🧠 为什么 Python ↔ C++ 能互通? **秘密在 `.msg` 文件**。`std_msgs/String.msg` 定义了消息结构,`rosidl` 自动生成 Python 类和 C++ 类,**接口完全对应**: | Python | C++ | |---|---| | `String()` | `std_msgs::msg::String()` | | `msg.data` | `msg.data` | | 字符串类型 | `std::string` | DDS 传输的是**二进制数据**(按 `.msg` 定义序列化),不关心语言。所以 py 发 cpp 收完全没问题。 --- ## 📚 深入学习 - [ROS2 C++ 教程](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Cpp-Publisher-And-Subscriber.html) - [doc/CODING_STYLE.md](../../doc/CODING_STYLE.md) — C++ 编码规范 - [py_pubsub README](../py_pubsub/README.md) — 对比看效果 --- ## ⏭️ 下一个包 学完这个,继续学 **[py_srv](../py_srv/README.md)** — 学习 Service(请求-响应)模式。 --- ## 📍 学习路径导航 | ⏮ 上一个 | 🏠 当前位置 | ⏭ 下一个 | |---|---|---| | [py_pubsub — Python Topic](../py_pubsub/README.md) | **cpp_pubsub — C++ Topic** | [py_srv — Python Service](../py_srv/README.md) | 📍 完整 12 包学习顺序见 [主 README](../../README.md#-12-包推荐学习顺序)