Maker.io main logo

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

Intro to ROS Part 4: Nodes, Topics, Publishers, and Subscribers (C++)

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

Intro to ROS Part 4: Nodes, Topics, Publishers, and Subscribers (C++)

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:

Copy Code
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:

Copy Code
<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:

Copy 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 line:

Copy Code
<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:

Copy Code
# 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:

Copy Code
# 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:

Copy Code
colcon build --packages-select my_cpp_pkg 
source install/setup.bash

Now run the publisher node:

Copy Code
ros2 run my_cpp_pkg minimal_publisher

Open another terminal and check the topic:

Copy Code
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.

Intro to ROS Part 4: Nodes, Topics, Publishers, and Subscribers (C++)

Writing the Subscriber Node in C++

Create another file: src/minimal_subscriber.cpp and write your subscriber node:

Copy Code
#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:

Copy Code
# 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:

Copy Code
colcon build --packages-select my_cpp_pkg

 

Then run both nodes in separate terminals:

Copy Code
source install/setup.bash 
ros2 run my_cpp_pkg minimal_publisher 
Copy Code
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:

Intro to ROS Part 4: Nodes, Topics, Publishers, and Subscribers (C++)

Visualizing Node Connections

Run rqt_graph to view the live topic graph:

Copy Code
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:

Copy Code
ros2 run my_py_pkg minimal_subscriber

Intro to ROS Part 4: Nodes, Topics, Publishers, and Subscribers (C++)

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!

Mfr Part # 28009
EXPERIENTIAL ROBOTICS PLATFORM (
SparkFun Electronics
$119.95
View More Details
Mfr Part # SC0194(9)
SBC 1.5GHZ 4 CORE 4GB RAM
Raspberry Pi
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.