13 KiB
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 就行,不必学这个包。
🎯 学完之后你能做什么?
- ✅ 用 C++ 写 ROS2 节点(知道
rclcpp::Node是什么) - ✅ 理解 Python ↔ C++ 跨语言互通(为什么能行?)
- ✅ 知道
shared_ptr/std::bind/rclcpp::QoS这些 C++ 套路 - ✅ 读懂 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(告诉编译器怎么编译)。
🚀 跑起来
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 互通):
# 终端 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<T> |
共享指针(自动内存管理) | 不用写,GC 自动管 |
::SharedPtr |
rclcpp 的共享指针类型 | 无 |
std::make_shared<T>(...) |
创建 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<T>(topic, qos) |
模板参数 T 是消息类型 | create_publisher(T, topic, qos) |
<std_msgs::msg::String> |
嵌套命名空间,ROS 消息类型 | String(从 std_msgs.msg 导入) |
std::bind 是 C++ 的"高阶函数",把方法 + 对象绑在一起,让定时器能回调。Python 里 lambda 就能搞定。
📖 Publisher 完整代码
// chatter_publisher.hpp - 类声明
#pragma once
#include <memory>
#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<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
std::size_t count_;
};
} // namespace cpp_pubsub
// chatter_publisher.cpp - 类实现
#include "cpp_pubsub/chatter_publisher.hpp"
#include <chrono>
#include <string>
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<std::string>(
"message_prefix", "Hello World C++: ",
rcl_interfaces::msg::ParameterDescriptor().set__description("消息前缀"));
this->declare_parameter<double>(
"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
// 模板参数 <std_msgs::msg::String> 表示消息类型
// 第二个参数 10 是队列大小(跟 Python 一样)
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter_cpp", 10);
// 4) 创建定时器
// std::bind 把方法 + this 绑成一个"函数对象"
// 让定时器能调用 ChatterPublisher::timer_callback
timer_ = this->create_wall_timer(
std::chrono::milliseconds(static_cast<int>(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 命名约定。返回自身引用,可以链式调用:
ParameterDescriptor().set__description("...") // 链式风格
// 等价于
auto d = ParameterDescriptor();
d.set__description("...");
📖 Subscriber 完整代码
// chatter_subscriber.hpp
#pragma once
#include <memory>
#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<std_msgs::msg::String>::SharedPtr subscription_;
std::size_t received_count_;
};
} // namespace cpp_pubsub
// chatter_subscriber.cpp
#include "cpp_pubsub/chatter_subscriber.hpp"
#include <string>
namespace cpp_pubsub {
ChatterSubscriber::ChatterSubscriber(const rclcpp::NodeOptions & options)
: rclcpp::Node("chatter_subscriber_cpp", options), received_count_(0)
{
this->declare_parameter<std::string>(
"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<std_msgs::msg::String>(
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:
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 函数(单独文件)
// chatter_publisher_main.cpp
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv); // 初始化 ROS2
rclcpp::spin(std::make_shared<cpp_pubsub::ChatterPublisher>()); // 创建节点 + spin
rclcpp::shutdown(); // 关闭 ROS2
return 0;
}
为什么 main 单独一个文件:类实现要单独编译给测试用(gtest 链接时会出冲突,如果 main 在类实现里)。
spin 是什么:死循环,反复调用所有 callback(定时器、订阅者、Service...)。按 Ctrl+C 退出。
📖 CMakeLists.txt 怎么读?
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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>)
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 路径、依赖)。
🧪 跑测试
colcon test --packages-select cpp_pubsub
colcon test-result --all --verbose
预期:cpp_pubsub: gtest 3/3 ✓ 全部通过。
🔧 自己改代码
- 改
src/chatter_publisher.cpp(比如改默认消息前缀) - 必须重编译(C++ 不会自动生效):
colcon build --packages-select cpp_pubsub - 重跑 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++ 教程
- doc/CODING_STYLE.md — C++ 编码规范
- py_pubsub README — 对比看效果
⏭️ 下一个包
学完这个,继续学 py_srv — 学习 Service(请求-响应)模式。
📍 学习路径导航
| ⏮ 上一个 | 🏠 当前位置 | ⏭ 下一个 |
|---|---|---|
| py_pubsub — Python Topic | cpp_pubsub — C++ Topic | py_srv — Python Service |
📍 完整 12 包学习顺序见 主 README