Intro to ROS Part 4: Nodes, Topics, Publishers, and Subscribers (C++)
2025-10-16 | By ShawnHymel
Single Board Computers Raspberry Pi SBC
In this tutorial, we’ll walk through how to write ROS 2 nodes in C++ that communicate using the publish/subscribe model. This mirrors what we did with Python in the previous episode, but this time you’ll learn how to structure C++ packages, manage dependencies with CMake, and use rclcpp to build high-performance nodes in C++.
By the end of this tutorial, you’ll know how to:
- Create a new C++ ROS 2 package
- Write publisher and subscriber nodes in C++
- Build and run your nodes using colcon
- Use rqt_graph to visualize node communication
The Docker image and code for this series can be found here: https://github.com/ShawnHymel/introduction-to-ros
Python vs. C++ in ROS 2
ROS 2 supports both Python and C++ out of the box. Python is easier to write and great for quick prototyping, especially with its dynamic typing and access to AI/ML libraries. However, Python has performance limitations: it’s generally slower and limited in multithreading due to the Global Interpreter Lock (GIL). Note that the GIL might be optional in future versions of Python, allowing for true multithreading within a process.
C++, by contrast, is statically typed and compiled, offering higher performance, deterministic behavior, and more control over low-level resources like memory and CPU. That makes it ideal for real-time applications or embedded systems. The tradeoff is that C++ is more verbose and has a steeper learning curve.
Thankfully, ROS 2 allows you to mix and match these languages. You can have Python and C++ nodes in the same system, communicating over shared topics or services.
Understanding ROS 2 Nodes and Topics
At the heart of any ROS 2 system are nodes: independent processes that perform specific functions such as collecting data from sensors, executing control algorithms, or sending commands to actuators. In C++, you define a node by creating a class that inherits from the rclcpp::Node base class. This object-oriented design allows for encapsulation, reusability, and a clear separation of concerns.
Nodes interact with one another by passing messages over named channels called topics. A publisher sends messages to a topic, while a subscriber receives messages from that same topic. This publish/subscribe model enables asynchronous, decoupled communication between components. Because publishers and subscribers are unaware of each other's existence, it's easy to scale and modify the system without needing to refactor large portions of code.
For example, a node that measures temperature might publish readings to a topic called /temperature. Other nodes may subscribe to that topic and receive updates as they are published. This design pattern is essential for creating robust and scalable robotic applications.
The rclcpp API makes it straightforward to implement this model in C++. You simply declare your topic, specify the message type (e.g., example_interfaces::msg::String), and create a publisher or subscriber object within your node class. You'll also define callback functions to handle incoming messages or generate outgoing data.
Setting Up the C++ ROS Package
Start by launching the Docker container you’ve been using for this course. Inside the container, create a new C++ package:
cd /config/workspace/src ros2 pkg create --build-type ament_cmake my_cpp_pkg
This command creates a new ROS 2 package with the ament_cmake build type. You’ll notice the directory structure includes:
- src/ – where you’ll place your source code
- include/my_cpp_pkg/ – for public headers
- package.xml – declares dependencies and metadata
- CMakeLists.txt – your CMake build script
Open package.xml and add dependencies:
<buildtool_depend>ament_cmake</buildtool_depend> <depend>rclcpp</depend> <depend>example_interfaces</depend>
Writing the Publisher Node in C++
Create a new file at src/minimal_publisher.cpp with the following code:
#include "rclcpp/rclcpp.hpp" #include "example_interfaces/msg/string.hpp" /** * Publisher example that periodically sends out a string */ class MinimalPublisher : public rclcpp::Node { public: /** * Constructor (call Node class constructor with node name) */ MinimalPublisher() : Node("minimal_publisher") { // Create a publisher object publisher_ = this->create_publisher<example_interfaces::msg::String>( "my_topic", 10); // Periodically call method timer_ = this->create_wall_timer( std::chrono::milliseconds(500), std::bind(&MinimalPublisher::timer_callback, this)); // Counter for messages sent counter_ = 0; } private: /** * Publishes simple message to topic */ void timer_callback() { // Fill out String message auto msg = example_interfaces::msg::String(); msg.data = "Hello, world: " + std::to_string(counter_); // Publish message to topic publisher_->publish(msg); RCLCPP_INFO(this->get_logger(), "Publishing: %s", msg.data.c_str()); // Increment counter counter_++; } // Declare member variables rclcpp::Publisher<example_interfaces::msg::String>::SharedPtr publisher_; rclcpp::TimerBase::SharedPtr timer_; size_t counter_; }; /** * Main entrypoint */ int main(int argc, char * argv[]) { // Initialize ROS 2 rclcpp::init(argc, argv); // Initialize and run node auto node = std::make_shared<MinimalPublisher>(); rclcpp::spin(node); // Cleanup rclcpp::shutdown(); return 0; }
This node uses C++ smart pointers and std::bind to schedule periodic publishing. Each message is logged to the console with RCLCPP_INFO, a thread-safe macro that supports severity levels.
If you have not used smart pointers before, I recommend reading about them here: https://medium.com/@lucky_rydar/guide-over-smart-pointers-in-c-46ed8b04448c.
Update package.xml
In your package, package.xml contains information used by the ROS system: metadata about the package and dependencies on other ROS packages. Every time you include another ROS package in your code, you should update the package.xml in your package.
This is often considered a “best practice” as your code may still compile and run without it! But you might run into bugs later on, as ROS uses this dependency information when running packages.
Add the following dependencies after <buildtool_depend> the
<depend>rclcpp</depend> <depend>example_interfaces</depend>
Update CMakeLists.txt
To build your executable, edit CMakeLists.txt and add the following to the “find dependencies” section:
# find dependencies find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(example_interfaces REQUIRED) find_package(my_interfaces REQUIRED)
Just before “ament_package()” add:
# Define minimal_publisher target and link libraries add_executable(minimal_publisher src/minimal_publisher.cpp) ament_target_dependencies(minimal_publisher rclcpp example_interfaces) install(TARGETS minimal_publisher DESTINATION lib/${PROJECT_NAME})
Build and Run the Package
Save your changes, then return to the root of your workspace and build the package:
colcon build --packages-select my_cpp_pkg source install/setup.bash
Now run the publisher node:
ros2 run my_cpp_pkg minimal_publisher
Open another terminal and check the topic:
source install/setup.bash ros2 topic echo /my_topic
You should see the “Hello, world” message and the counter value being printed in both terminals.
Writing the Subscriber Node in C++
Create another file: src/minimal_subscriber.cpp and write your subscriber node:
#include "rclcpp/rclcpp.hpp" #include "example_interfaces/msg/string.hpp" /** * Subscriber example that prints messages to the console */ class MinimalSubscriber : public rclcpp::Node { public: /** * Constructor (call Node class constructor with node name) */ MinimalSubscriber() : Node("minimal_subscriber") { // Create a subscription object subscription_ = this->create_subscription<example_interfaces::msg::String>( "my_topic", 10, std::bind( &MinimalSubscriber::listener_callback, this, std::placeholders::_1)); } private: /** * Prints message to the console */ void listener_callback(const example_interfaces::msg::String & msg) { RCLCPP_INFO(this->get_logger(), "Received message: '%s'", msg.data.c_str()); } // Declare member variables rclcpp::Subscription<example_interfaces::msg::String>::SharedPtr subscription_; }; /** * Main entrypoint */ int main(int argc, char * argv[]) { // Initialize ROS 2 rclcpp::init(argc, argv); // Initialize and run node auto node = std::make_shared<MinimalSubscriber>(); rclcpp::spin(node); // Cleanup rclcpp::shutdown(); return 0; }
Update CMakeLists.txt again:
# Define minimal_subscriber target and link libraries add_executable(minimal_subscriber src/minimal_subscriber.cpp) ament_target_dependencies(minimal_subscriber rclcpp example_interfaces) install(TARGETS minimal_publisher minimal_subscriber DESTINATION lib/${PROJECT_NAME})
Rebuild and source your workspace:
colcon build --packages-select my_cpp_pkg
Then run both nodes in separate terminals:
source install/setup.bash ros2 run my_cpp_pkg minimal_publisher
source install/setup.bash ros2 run my_cpp_pkg minimal_subscriber
You should see your subscriber node printing out the message being sent from the publisher:
Visualizing Node Connections
Run rqt_graph to view the live topic graph:
rqt_graph
You’ll see both the minimal_publisher and minimal_subscriber nodes connected by the my_topic channel. You can also run a Python subscriber with your C++ publisher to demonstrate cross-language compatibility:
ros2 run my_py_pkg minimal_subscriber
Wrapping Up
In this tutorial, we recreated the basic publisher/subscriber example from Python in C++. You learned about the structure of a C++ ROS 2 package, how to define publishers and subscribers using rclcpp, and how to build and run them using colcon. This foundational knowledge sets you up to build more advanced ROS 2 applications that leverage both performance and flexibility.
In the next episode, we’ll take this a step further and implement ROS 2 services in both Python and C++.
Stay tuned!