Files
ROS2_learn/src/cpp_pubsub

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(告诉编译器怎么编译)。


🚀 跑起来

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 ✓ 全部通过。


🔧 自己改代码

  1. src/chatter_publisher.cpp(比如改默认消息前缀)
  2. 必须重编译(C++ 不会自动生效):
    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 收完全没问题。


📚 深入学习


⏭️ 下一个包

学完这个,继续学 py_srv — 学习 Service(请求-响应)模式。


📍 学习路径导航

⏮ 上一个 🏠 当前位置 ⏭ 下一个
py_pubsub — Python Topic cpp_pubsub — C++ Topic py_srv — Python Service

📍 完整 12 包学习顺序见 主 README