3.6.3. rclcpp, the C++ client library#
In Section 3.6.2 you worked through the basics of the Python ROS client library, rclpy.
In this section we will mirror the steps of that section, but now using the C++ ROS client library, rclcpp.
Note
In this section you will build simple ROS nodes in C++
using standard C++ build tools such as cmake and make,
without creating a ROS package.
In Section 3.7.4 you will learn how to setup a C++ ROS package
and use colcon to build your ROS projects.
3.6.3.1. Publishing Topics in C++#
Now we will write the same talker and listener pair as we did in Python, but this time using C++.
Here is a complete C++ program that publishes String messages:
#include <chrono>
#include <iostream>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("my_publisher");
auto pub = node->create_publisher<std_msgs::msg::String>("chatter", 10);
// Use lambda function to create callback using the node and publisher we created
auto cb_timer = [node, pub]() -> void
{
// Get time reported by node
auto now = node->get_clock()->now();
double now_in_seconds = now.seconds();
// Set the std_msgs/msg/String message
auto message = std_msgs::msg::String();
message.data = "Hello World, the time is: " + std::to_string(now_in_seconds);
// Publish the message
std::cout << "Publishing: '" << message.data << "'" << std::endl;
pub->publish(message);
};
auto timer = rclcpp::create_timer(
node, node->get_clock(), std::chrono::milliseconds(500), cb_timer);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
Copy the C++ code into a file called talker.cpp.
We will discuss this source code in more detail in a moment in Section 3.6.3.3.
Of course, as with all C++ programs, we need to compile this program before we can run it. Managing the compilation commands for C++ is cumbersome, so we use CMake to help us.
Here is a complete CMake build file that allows us to build the talker node.
Save this in a file called CMakeLists.txt (the default name for CMake build files):
cmake_minimum_required(VERSION 3.5)
project(talker_listener)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker talker.cpp)
target_include_directories(talker PRIVATE ${rclcpp_INCLUDE_DIRS} ${std_msgs_INCLUDE_DIRS})
target_link_libraries(talker ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})
Note
Let’s briefly analyse this CMakeLists.txt file:
The first two lines indicate that these instructions are intended for at least CMake version 3.5, and that this build file will define a new build project called
talker_listener.The next two lines with
find_package(..)tell CMake this project will require additional configuration options to support usingrclcppandstd_msgs, which will provide the C++ ROS client API and ROSstd_msgsinterface specifications in C++. These CMake packages will set additional variables, such asrclcpp_INCLUDE_DIRSandstd_msgs_INCLUDE_DIRSthat define additional C++ directories containing the header files that should be found by C++’s#include <...>statements. The variablesrclcpp_LIBRARIESandstd_msgs_LIBRARIESwill specify which libraries should be linked to use these packages.The next line tells CMake it should build an executable using a single source file:
talker.cpp. The general syntax isadd_executable(<executable_name> <cpp_file1> <cpp_file2> ...). So, the line here defines an executabletalkerbased on a single source filetalker.cpp.The final two lines tell CMake that the
talkerexecutable will require the additional include directories and libraries provided byrclcppandstd_msgs.
Ok, let’s try to build the project now.
Let’s make a build/ directory from where to execute the build process:
$ mkdir build
$ cd build
Warning
Building executables with C++ will generate a lot of intermediate files and directories. Keeping such intermediate files around is useful, because then we can skip some processing steps if we later need to rebuild our C++ project. However, it is not so nice to generate all these temporary files in the same directory as our source code, because it makes it harder for us to locate our source files, we might inadvertently add such derived files to our Git repository, or we might accidentally delete our source files when we try to cleanup the derived files.
Therefore, before we attempt to build our executable, it is good practice to create a separate directory
from where we execute the build steps, such that all temporary files are stored there.
A common name for such a directory is build/.
From within this (still) empty build directory, we can now invoke cmake
and point it to the directory where the CMakeLists.txt file is located.
Then cmake will generate the necessary setup files inside our current working directory.
Since that working directory is build/ and the CMakeLists.txt is “one directory up”,
we point cmake to our working directory’s parent directory using ..:
$ cmake ..
$ ls
ament_cmake_core/ ament_cmake_package_templates/ ament_cmake_uninstall_target/ CMakeCache.txt CMakeFiles/
cmake_install.cmake CTestConfiguration.ini CTestCustom.cmake CTestTestfile.cmake Makefile
As we can see cmake generated a lot of additional configuration and setup files for us.
Also note that there is now a file called Makefile.
This Makefile contains the actual compiler and linker commands to build our program.
We can now build the program using the make program which will look for Makefile.
From within the build/ directory containing the Makefile, simply run:
$ make
The make program will now work through the building recipe in the Makefile and generate the executable.
If you make changes to the source files, you would just need to rerun this make command to rebuild the executable.
The cmake instruction would only need to be rerun if you make changes to original the CMakeLists.txt file,
for instance if you add additional executables to build or add new C++ source files to be used in an executable.
After make is finished, you should end up with a compiled executable called talker in the build/ directory.
Still from the build/ directory, run the talker:
$ ./talker
You should see:
Publishing: 'Hello World, the time is: 1728380337.162038'
Publishing: 'Hello World, the time is: 1728380337.661520'
Publishing: 'Hello World, the time is: 1728380338.161462'
Keep the talker running and another shell try ros2 topic to listen
in:
$ ros2 topic echo chatter
You should see (numbers will vary depending on timing between the two commands):
data: 'Hello World, the time is: 1728380545.713562'
---
data: 'Hello World, the time is: 1728380546.213435'
---
data: 'Hello World, the time is: 1728380546.713456'
Note
A benefit of using a separate build/ directory from our source code
is that if we somehow mess up the temporary files,
we can simply remove this one directory and make a clean new build.
First, you would delete the build directory (rm -r build/),
and then repeat the standard building steps:
mkdir build
cd build
cmake ..
make
3.6.3.2. Subscribing to Topics in C++#
Next, we can also write our own listener to use in place of ros2 topic.
Here is a complete C++ code for a listener ROS node
which subscribes to string messages and prints them to console:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
void cb(const std_msgs::msg::String::SharedPtr msg)
{
std::cout << "I heard: " << msg->data << std::endl;
}
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("my_subscriber");
auto sub = node->create_subscription<std_msgs::msg::String>("chatter", 10, cb);
rclcpp::spin(node);
return 0;
}
If you are still in the build/ subdirectory, go back to the parent directory (cd ..) where your other source file is located
and copy the code block above into a new file called listener.cpp.
We now have to update our project configuration to build a second executable
from this single C++ source file.
To do this add some corresponding cmake instructions to the
bottom of our CMakeLists.txt file from earlier:
add_executable(listener listener.cpp)
target_include_directories(listener PRIVATE ${rclcpp_INCLUDE_DIRS} ${std_msgs_INCLUDE_DIRS})
target_link_libraries(listener ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})
Note
The CMakeLists.txt should now have 2 lines with add_executable(...),
namely one to define the talker executable,
and one to define the listener executable.
Re-enter the build/ directory, and configure and build your C++ project again:
$ cd build/
$ cmake ..
$ make
Now you should have also have a listener executable. With your talker
still running in one shell, start up your listener in another shell:
$ ./listener
You should see (again, numbers will vary depending on timing):
I heard: "Hello World, the time is: 1728380337.661520"
I heard: "Hello World, the time is: 1728380338.161462"
I heard: "Hello World, the time is: 1728380338.661460"
3.6.3.3. Digging into the C++ Code#
Now that we know these programs work, we can dig into their code. Both programs start with the same preamble:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
We always need to include the rclcpp client library, which gives us
much of what we need to write ROS applications in C++. But we also need to
specifically import the ROS message type(s) that we will use. In this
case we are using the simple std_msgs/String message, which contains a
single field called data, of type string. If we wanted to use the
sensor_msgs/Image message, which represents camera images, then we
would #include "sensor_msgs/msg/image.hpp".
After the imports, both programs perform common initialization:
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("my_node_name");
We initialize the rclcpp library and then call into it to create a
Node object via a shared pointer, giving the node its name in the contructor.
Subsequently we will operate on that Node object.
In the talker, we use the Node object to create a shared pointer to a Publisher object:
auto pub = node->create_publisher<std_msgs::msg::String>("chatter", 10);
We declare via template the type of data we will publish (std_msgs/String), the
name of the topic on which we will publish (chatter), and the maximum
number of outbound messages to locally queue up (10). That last
argument comes into play when we are publishing faster than subscribers
are consuming the data.
The equivalent step in the listener is to create a shared pointer to a Subscription object:
auto sub = node->create_subscription<std_msgs::msg::String>("chatter", 10, cb);
The type (String) and topic name (chatter) arguments have the same meaning
as for the create_publisher() call, and the numerical argument (10) is
setting an analogous maximum queue size for inbound messages. The key
difference is the cb argument, which refers to this callback
function that we also defined in the listener:
void cb(const std_msgs::msg::String::SharedPtr msg)
{
std::cout << "I heard: " << msg->data << std::endl;
}
That function will be called whenever the listener receives a message, and a shared pointer to the received message will be passed in as an argument. In this case we simply print the content to console.
Note
You can see that the type of the msg argument is std_msgs::msg::String::SharedPtr.
This type is just a shorthand for std::shared_ptr<std_msgs::msg::String>, the shared pointer type for ROS’s String message format.
Many ROS classes provide such shorthands for their shared pointers, for instance:
rclcpp::Node::SharedPtris a shorthand forstd::shared_ptr<rclcpp::Node>rclcpp::Publisher::SharedPtris a shorthand forstd::shared_ptr<rclcpp::Publisher>rclcpp::Subscriber::SharedPtris a shorthand forstd::shared_ptr<rclcpp::Subscriber>etc.
With the callback defined and the Subscription created, the rest of
the listener is one line:
rclcpp::spin(node);
This call hands control over to rclcpp to wait for new messages to
arrive (and more generally for events to occur) and invoke our callback.
Back in the talker, we create a callback function for a timer to call.
In this example, we used a C++11 lambda function to define the callback.
The callback will need to access node to get the node’s current timestamp,
and it will need to access the publisher pub to publish the message.
Therefore, we let the lambda function capture the node and pub shared pointers.
The callback function itself takes no arguments and should return void,
as this is the function signature rclcpp::create_timer expects for the callback.
// Use lambda function to create callback using the node and publisher we created
auto cb_timer = [node, pub]() -> void {
// Get time reported by node
auto now = node->get_clock()->now();
double now_in_seconds = now.seconds();
// Set the std_msgs/msg/String message
auto message = std_msgs::msg::String();
message.data = "Hello World, the time is: " + std::to_string(now_in_seconds);
// Publish the message
std::cout << "Publishing: \"" << message.data << "\"" << std::endl;
pub->publish(message);
};
Once the callback is defined, we can setup the timer for our node,
where the delay between subsequent calls can be defined using objects from std::chrono that represent time durations:
auto timer = rclcpp::create_timer(node, node->get_clock(), std::chrono::milliseconds(500), cb_timer);
Finally, the main loop for our talker can also be executed:
rclcpp::spin(node);
In these steps we create a message object then on each iteration of the loop we update the message content and publish it, sleeping briefly between iterations.
3.6.3.4. Summary#
That completes the (very) brief introduction to the C++ ROS client library.
We only covered the basics of using topics, but again not services, actions, parameters, or the many other facets of ROS.
You have implemented two ROS nodes and configured the CMakeLists.txt to build the executables,
but these nodes were not part of a ROS package yet.
In a later section, you will learn to implement your C++ nodes as part of a custom ROS 2 package,
which is the recommended workflow for ROS development.
As a final note, it may be useful to know that the online ROS developer documentation contains an API reference of the rclcpp library (if you prefer the Doxygen style, see this alternative),
which you could consult if you are looking for a list of available C++ functions, classes, namespaces and other features. For example, you could use it to look up with other member functions the Node class provides.
However, be aware that this API reference is pretty technical, using advanced C++ concepts, and it discusses various complex ROS concepts beyond what this manual will cover.
As was stated before, the best way to learn about ROS concepts, patterns,
and conventions is to start reading and writing C++ ROS code.
Once you become more experienced with ROS programming, the API reference can become a valuable resource to implement more advanced ROS features in your projects.