With the transition to use IDL
for specifying interfaces in ROS 2 Dashing this article has been superseded by the Interface Definition and Language Mapping article.
This article describes the generated C++ code for ROS 2 interfaces.
Authors: Dirk Thomas
Date Written: 2015-06
Last Modified: 2019-03
This article specifies the generated C++ code for ROS interface types defined in the interface definition article.
All code of a ROS package should be defined in a namespace named after the package. To separate the generated code from other code within the package it is defined in a sub namespace:
<package_name>::msg
.<package_name>::srv
.NOTE: Using the additional sub namespace ensures that the symbols are different and don’t overlap with the ROS 1 symbols.
That allows to include both in a single compilation unit like the ros1_bridge
.
Following the C++ style guide of ROS 2 the namespace hierarchy is mapped to a folder structure.
The filenames use lowercase alphanumeric characters with underscores for separating words and end with either .hpp
or .cpp
.
For a message a templated struct
with the same name followed by an underscore is generated.
The single template argument is the allocator for the data structure.
For ease of use there is a typedef
with the same name as the message which uses a default allocator (e.g. std::allocator
).
For each message two files are being generated:
<my_message_name>.hpp
currently only includes <my_message_name>__struct.hpp
<my_message_name>__struct.hpp
containing the definition of the structThis allows to add additional files besides the one with the suffix __struct
to provide additional functionality.
For each additional functionality it can be decided to include it from the first header file.
TODO: specify content of <my_message_name>__traits.hpp
file
ROS type | C++ type |
---|---|
bool | bool |
byte | uint8_t |
char | char |
float32 | float |
float64 | double |
int8 | int8_t |
uint8 | uint8_t |
int16 | int16 |
uint16 | uint16 |
int32 | int32 |
uint32 | uint32 |
int64 | int64 |
uint64 | uint64_t |
string | std::string |
ROS type | C++ type |
---|---|
static array | std::array<T, N> |
unbounded dynamic array | std::vector |
bounded dynamic array | custom_class<T, N> |
bounded string | std::string |
The struct has same-named public member variables for every field of the message.
For each field a typedef
is created which is named after the member with a leading underscore and a trailing _type
.
Numeric constants are defined as enums
within the struct.
All other constants are declared as static const
members in the struct and their values are defined outside of the struct.
In the following discussion, “member” refers to the class member in the C++ class while “field” refers to the field definition in the IDL file.
The default constructor initializes all members with the default value specified in the IDL file, or otherwise with the common default for the field type as defined in this article (note: char
fields are considered numeric for C++).
In some cases this may not be desirable, since these fields will often be immediately overwritten with user-provided values.
Therefore, the constructor takes an optional directive of type rosidl_generator_cpp::MessageInitialization
to control how initialization is done:
MessageInitialization::ALL
- Initialize each member with the field’s default value specified in the IDL file, or otherwise with the common default for the field type as defined in this article (note: char
fields are considered numeric for C++).
MessageInitialization::SKIP
- Don’t initialize any members; it is the user’s responsibility to ensure that all members get initialized with some value, otherwise undefined behavior may result
MessageInitialization::ZERO
- Zero initialize all members; all members will be value-initialized (dynamic size or upper boundary arrays will have zero elements), and default values from the message definition will be ignored
MessageInitialization::DEFAULTS_ONLY
- Initialize only members that have field default values; all other members will be left uninitialized
Optionally the constructor can be invoked with an allocator.
The struct has no constructor with positional arguments for the members. The short reason for this is that if code would rely on positional arguments to construct data objects changing a message definition would break existing code in subtle ways. Since this would discourage evolution of message definitions the data structures should be populated by setting the members separately, e.g. using the setter methods.
For each field a setter method is generated to enable method chaining.
They are named after the fields with a leading set__
.
The setter methods have a single argument to pass the value for the member variable.
Each setter method returns the struct itself.
The comparison operators ==
and !=
perform the comparison on a per member basis.
The struct contains typedefs
for the four common pointer types plain pointer
, std::shared_ptr
, std::unique_ptr
, std::weak_ptr
.
For each pointer type there a non-const and a const typedef
:
RawPtr
and ConstRawPtr
SharedPtr
and ConstSharedPtr
UniquePtr
and ConstUniquePtr
WeakPtr
and ConstWeakPtr
For similarity to ROS 1 the typedefs
Ptr
and ConstPtr
still exist but are deprecated.
In contrast to ROS 1 they use std::shared_ptr
instead of Boost.
For a service a struct
with the same name followed by an underscore is generated.
The struct contains only two typedefs
:
Request
which is the type of the request part of the serviceResponse
which is the type of the request part of the serviceThe generated code is split across multiple files the same way as message are.
For the request and response parts of a service separate messages are being generated.
These messages are named after the service and have either a _Request
or _Response
suffix.
They are are still defined in the srv
sub namespace.