ROS-Ready

3. Publisher & Subscriber C++

Writing your first ROS 2 publisher and subscriber in C++ with rclcpp

5 min read

What will you learn in this unit?

  • How to create a publisher
  • How to create a subscriber
  • What are topic messages and how they work
ROS 2 publisher subscriber diagram

A publisher sends messages of a fixed type onto a named topic. Any number of subscribers can listen on that topic and run a callback whenever a new message arrives. The publisher and subscribers never talk to each other directly — they only know the topic name. In ROS 2 there is no central master: DDS (Data Distribution Service) takes over matchmaking. Each node advertises what it publishes or subscribes to via multicast, and once peers discover each other, message data flows directly peer-to-peer.

Creating a ROS 2 Package

Let's create a package called cpp_tutorial. Execute the following commands in the terminal:

First, navigate to the src folder in your workspace:

bash
cd ~/ros2_ws/src

Create the package with the ros2 pkg create command and set the dependencies to rclcpp and std_msgs:

bash
ros2 pkg create --build-type ament_cmake --dependencies rclcpp std_msgs cpp_tutorial

Navigate back to the ros2_ws:

bash
cd ~/ros2_ws

Now since we are using C++ we need to build the package:

bash
colcon build --packages-select cpp_tutorial --symlink-install

Source the workspace:

bash
source install/setup.bash

Here you can check if the package was created successfully:

bash
ros2 pkg list | grep cpp_tutorial

Now navigate to the package:

bash
cd ~/ros2_ws/src/cpp_tutorial

Create a file for the publisher and subscriber in the src folder:

bash
touch src/publisher.cpp
bash
touch src/subscriber.cpp

Publisher

  1. Now let's create the publisher. Copy the following code to the publisher.cpp file:
cpp
#include <chrono>                       // For the 500ms literal below
#include <memory>                       // For std::make_shared
#include "rclcpp/rclcpp.hpp"            // The ROS 2 C++ client library
#include "std_msgs/msg/int32.hpp"       // The Int32 message from std_msgs

using namespace std::chrono_literals;

class CounterPublisher : public rclcpp::Node
{
public:
    CounterPublisher() : Node("counter_publisher"), count_(0)
    {
        // Create a Publisher on the /counter topic with depth 10
        publisher_ = this->create_publisher<std_msgs::msg::Int32>("counter", 10);
        // Timer fires every 500 ms → publish rate of 2 Hz
        timer_ = this->create_wall_timer(500ms, std::bind(&CounterPublisher::on_tick, this));
    }

private:
    void on_tick()
    {
        auto msg = std_msgs::msg::Int32();
        msg.data = count_++;
        publisher_->publish(msg);
    }

    rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
    int count_;
};

int main(int argc, char ** argv)
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<CounterPublisher>());  // Keep the node alive
    rclcpp::shutdown();
    return 0;
}
  1. Since C++ is a compiled language, we need to modify the CMakeLists.txt in the cpp_tutorial package to build the code. For more about CMakeLists in ROS 2 see: https://docs.ros.org/en/humble/How-To-Guides/Ament-CMake-Documentation.html

Here's how the CMakeLists.txt should look:

cmake
cmake_minimum_required(VERSION 3.8)
project(cpp_tutorial)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find_package() is how ament tells CMake where the ROS 2 libraries live.
# Every build dependency listed in package.xml needs a find_package() line here.
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

## Build ##

# add_executable() defines one binary called "publisher" built from the source below.
add_executable(publisher src/publisher.cpp)

# ament_target_dependencies() links and adds include paths for each ROS 2 dep in one call.
ament_target_dependencies(publisher rclcpp std_msgs)

## Install ##

# The install(TARGETS …) block is what makes `ros2 run cpp_tutorial publisher` work.
# It copies the binary into install/cpp_tutorial/lib/cpp_tutorial/ where the launcher looks.
install(TARGETS
  publisher
  DESTINATION lib/${PROJECT_NAME})

ament_package()

To run the node first build it and then execute the following commands in different terminals:

Build the Node

To build the node execute the following commands in the terminal:

Navigate to the ros2_ws directory:

bash
cd ~/ros2_ws

Build the workspace:

bash
colcon build --packages-select cpp_tutorial --symlink-install

Source the workspace:

bash
source ./install/setup.bash

Check if the package was loaded successfully:

bash
ros2 pkg list | grep cpp_tutorial

Run the Publisher

  1. Unlike ROS 1, there is no roscore to start. You can run the publisher directly:
bash
ros2 run cpp_tutorial publisher

Nothing happening? Well... that's not actually true! We just created a topic named /counter, and published through it an integer that increases indefinitely. Let's check this out.

A topic is like a pipe. Nodes use topics to publish information for other nodes so that they can communicate. You can find out, at any time, the number of topics in the system by doing a ros2 topic list. You can also check for a specific topic.

  1. Open a new terminal ([ctrl + b] and [Shift + 2]), source your workspace (source ~/ros2_ws/install/setup.bash) and, type ros2 topic list and check for a topic named /counter.
bash
ros2 topic list
output
/counter
/parameter_events
/rosout

And there is our topic /counter! And some other topics that ROS 2 creates by default for logging and parameter events.

  1. To request information about a topic type the following command in a terminal: ros2 topic info /counter
bash
ros2 topic info /counter
output
Type: std_msgs/msg/Int32
Publisher count: 1
Subscription count: 0
  1. The output indicates the type of the message (std_msgs/msg/Int32), how many publishers are on the topic, and how many subscribers. Use the -v flag to see QoS profiles and node names. Now, type ros2 topic echo /counter and check the output of the topic in realtime. The output will look something like this:
bash
ros2 topic echo /counter
output
data: 95
---
data: 96
---
data: 97
---
data: 98
---

To stop any process in the terminal, press [ctrl + c] and to close the terminal press [ctrl + d].

Subscriber

You've learned that a topic is a channel where nodes can either write or read information. You've also seen that you can write into a topic using a publisher. A subscriber is a node that reads information from a topic.

cpp
#include <memory>                       // For std::make_shared and std::placeholders
#include "rclcpp/rclcpp.hpp"            // The ROS 2 C++ client library
#include "std_msgs/msg/int32.hpp"       // The Int32 message from std_msgs

class CounterSubscriber : public rclcpp::Node
{
public:
    CounterSubscriber() : Node("counter_subscriber")
    {
        // Subscribe to /counter with QoS depth 10 and our callback
        subscription_ = this->create_subscription<std_msgs::msg::Int32>(
            "counter", 10,
            std::bind(&CounterSubscriber::on_message, this, std::placeholders::_1));
    }

private:
    void on_message(const std_msgs::msg::Int32 & msg) const
    {
        // RCLCPP_INFO is the ROS 2 equivalent of ROS_INFO_STREAM
        RCLCPP_INFO(this->get_logger(), "data: %d", msg.data);
    }

    rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr subscription_;
};

int main(int argc, char ** argv)
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<CounterSubscriber>());  // Keep running until Ctrl+C
    rclcpp::shutdown();
    return 0;
}

Modify the CMakeLists.txt to include the subscriber:

cmake
cmake_minimum_required(VERSION 3.8)
project(cpp_tutorial)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

## Build ##

add_executable(publisher src/publisher.cpp)
ament_target_dependencies(publisher rclcpp std_msgs)

add_executable(subscriber src/subscriber.cpp)
ament_target_dependencies(subscriber rclcpp std_msgs)

## Install ##

install(TARGETS
  publisher
  subscriber
  DESTINATION lib/${PROJECT_NAME})

ament_package()

Build the Node

Navigate to the ros2_ws directory:

bash
cd ~/ros2_ws

Build the workspace:

bash
colcon build --packages-select cpp_tutorial --symlink-install

Source the workspace:

bash
source ./install/setup.bash

Check if the package was loaded successfully:

bash
ros2 pkg list | grep cpp_tutorial

Run the Subscriber

  1. In ROS 2 you no longer need to start a roscore. Just run each node in its own terminal.

  2. In different terminals (source install/setup.bash in each one), run the following commands:

bash
ros2 run cpp_tutorial publisher
bash
ros2 run cpp_tutorial subscriber

You should see output similar to this:

output
[INFO] [counter_subscriber]: data: 188
[INFO] [counter_subscriber]: data: 189
[INFO] [counter_subscriber]: data: 190
[INFO] [counter_subscriber]: data: 191

You made it! You created a publisher and a subscriber in C++! Now you can create your own nodes and make them communicate with each other.

O.W.L. exam0 / 45 pts

Ordinary Wizarding Level

6 questions · 5 pts first try · 2 pts on a later try · +15 bonus for a clean sweep

Question 1 of 6

How often does this node publish?

timer_ = this->create_wall_timer(500ms, std::bind(&CounterPublisher::on_tick, this));

On this page