7.3. Useful C++ idioms#
Important
For RSP students, please work through the provided C++ manual first to ensure you are familiar with the basics of the C++ syntax and the C++ build system we use (CMake, compiling, linking).
C++ was first standardized in 1998 and that version of C++ is called C++98
.
Between 2003 and 2020 many new features were introduced in the C++ language and its Standard Library,
leading to different C++ versions, e.g., C++03
(from 2003), C++11
, C++14
, C++17
, etc.
While a lot of current C++ code (and documentation) relies on core language features available since C++98
,
it is beneficial to know several handy more modern language features that appear in many ROS 2 C++ examples.
This section provides a short overview of some useful modern C++ idioms, namely
7.3.1. The auto
keyword#
In C++ a variable’s type always needs to be defined when the variable is declared:
#include <iostream>
#include <string>
std::string greet(std::string name) {
return "hello " + name;
}
int main()
{
int x = 1;
double g = 9.8;
std::string message = greet("Alice");
std::cout << "x = " << x << std::endl;
std::cout << "g = " << g << std::endl;
std::cout << message << std::endl;
return 0;
}
In cases where the compiler can deduce the correct type, you can use the auto
keyword instead of the type,
so the above can also be achieved with:
#include <iostream>
#include <string>
auto greet(std::string name) {
return "hello " + name;
}
int main()
{
auto x = 1;
auto g = 9.8;
auto message = greet("Alice");
std::cout << "x = " << x << std::endl;
std::cout << "g = " << g << std::endl;
std::cout << message << std::endl;
return 0;
}
The auto
keyword can also be used to write more compact for-loops
over containers, such as std::vector
:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// Using auto with range-based for loop
for (auto element : vec) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
Important
Be aware of the following caveats:
auto
is not a new data type, nor will it make variables behave as in Python where you can change an existing variable’s type to a different type or class. Each variable still has a type as before (e.g.int
,double
or some class), you just don’t have to type it out explicitly anymore:
int main() {
auto x = 1; // ok, compiler will infer x is of type int
x = 2; // no problem, assigning 2 to a type int
x = "foo"; // will NOT work, cannot assign a string to a type int
return 0;
}
the compiler might not be able to figure out what you intended
auto
to be a stand-in for: e.g.,auto s = "hello";
will makes
of the typeconst char *
and not ofstd::string
;You cannot just put
auto
everywhere instead of a type, e.g. usingauto
in function arguments will not work because the compiler would not be able to deduce the type of the argument of the function:
// will NOT work
auto greet(auto name) {
return "hello " + name;
}
7.3.2. Function objects with std::function
#
In C++11, the Standard Library introduced several useful tools for handling functions and function-like objects. These tools provide greater flexibility and simplicity when working with callbacks, event handlers, or any scenario where you need to pass around functions. Accordingly, there are now several common ways for defining a callback function in C++:
Use a regular C++ function.
Create a function with
std::bind
, e.g., to use a class member function as a callback. This will be discussed in Section 7.3.3.Use a lambda function, a C++11 syntax to define anonymous functions within functions. This will be discussed in Section 7.3.4.
Unsurprisingly, you will encounter these concepts a lot when working later with the ROS 2 C++ API.
To facilitate working with these different types of functions, std::function
provides a unified wrapper.
It can store, copy, and call any callable target (like functions, lambda expressions, bind expressions, or other function objects) with a specified function signature (recall the signature consists of a function return type and argument types).
To use std::function
include the <functional>
header.
The general syntax for std::function
to refer to a function with arguments arg_types
and return type return_type
is std::function<return_type(arg_types)>
.
Here is a concrete example:
#include <iostream>
#include <functional>
// A simple function
void print_sum(int a, int b) {
std::cout << (a + b) << std::endl;
}
int main() {
// Declare a std::function
std::function<void(int, int)> func;
// Assign a function to std::function
func = print_sum;
// Call the function
func(3, 4); // Outputs 7
return 0;
}
Since we declared func
as a variable of type std::function<void(int, int)>
,
it can be used to refer to any function with arguments int, int
and return type void
,
such as the void print_sum(int a, int b)
function here (note: the names of the arguments do not matter, only the types).
Calling this func
object will just call the function that was assigned to it with the same argument values.
So func(3, 4)
simply calls print_sum(3, 4)
.
Since std::function
objects can be passed around,
we can also use them in the function signature of functions that need to use a callback.
In this example the use_callback
function has a std::function
argument,
which means we can call it on any regular C++ function with the required function signature:
#include <iostream>
#include <functional>
void print_sum(int a, int b) {
std::cout << (a + b) << std::endl;
}
void use_callback(const std::function<void(int, int)>& callback) {
callback(1, 2);
callback(3, 4);
callback(5, 6);
}
int main() {
use_callback(print_sum); // will print 3, 7, 11
return 0;
}
7.3.3. Binding function arguments with std::bind
#
std::bind
allows you to “freeze” some arguments of a function, creating a new function that requires fewer arguments.
Essentially, it creates a new function object by “binding” values for some arguments of a given function.
The resulting function object stores the given argument values and can then be used just as a regular function.
The basic syntax is:
#include <functional>
// Basic usage
auto new_function = std::bind(function, arg1, arg2, ..., _N);
In the above function
is the function to bind,
and arg1
, arg2
, …, _N
are its arguments to bind.
_N
is a placeholder that represents the position of unbound arguments in the new function.
We could declare new_function
to be a std::functional
object type explicitly,
but it is often more convenient to use the auto
keyword
and let the compiler deduce the correct type returned by std::bind
.
Here is an example of a simple add(a, b)
function with two arguments being reduced
to a function (object) add_ten(b)
with one argument by binding the value of a
to 10:
#include <iostream>
#include <functional>
// A simple function
int add(int a, int b) {
return a + b;
}
int main() {
// Bind the `add` function, setting its first argument to 10.
// Its second argument will become the first (_1) argument of the new function.
auto add_ten = std::bind(add, 10, std::placeholders::_1);
// Now add_ten only takes one argument
std::cout << add_ten(5) << std::endl; // Outputs 15
return 0;
}
A very useful feature of std::bind
is that it also allows you to bind a class member function to a specific object of that class, enabling their use as callbacks in functions that accept only standard function objects (e.g., std::function
).
This is particularly useful when working with libraries or APIs that require callback functions, such as the ROS C++ client library.
By using std::bind
, you can create a callable object that encapsulates both the member function and the instance of the class it should operate on.
This callable object can then be passed to functions expecting a callback, ensuring that the member function is called on the correct instance when the callback is triggered.
Here is an example:
#include <iostream>
#include <functional>
// A simple class with an internal state (counter) and a member function
class Printer {
int count_;
public:
void print_str(const std::string& data) {
std::cout << "Message " << count_ << ": " << data << std::endl;
count_++;
}
};
// A function that calls some callback function for strings
void use_callback(const std::function<void(const std::string&)>& callback) {
callback("First time calling callback!");
callback("Second time calling callback!");
callback("Third time calling callback!");
}
int main() {
Printer printer;
// Using std::bind to create a (const!) function object for the member function
auto bound_print_str = std::bind(&Printer::print_str, &printer, std::placeholders::_1);
// Passing the bound member function as a callback now WILL work
use_callback(bound_print_str);
return 0;
}
In this example use_callback
accepts a std::function
object for functions that with a single std::string
argument.
It then calls this callback three times with some input.
With the callback design, use_callback
does not need to know what the callback exactly does,
nor if it is a regular C++ function or a class member function.
Here we use std::bind
to pass the print_str
member function for the printer
object as the callback.
Each time use_callback
calls this callback,
the print_str
member function will update the internal message counter count_
of printer
, and output the message with the count to the standard output stream.
Compiling and running this C++ program should print on the terminal
Message 0: First time calling callback!
Message 1: Second time calling callback!
Message 2: Third time calling callback!
7.3.4. Lambda function syntax#
Lambda functions, introduced in C++11, are a convenient way to define anonymous functions directly in the code of another function. They are especially useful for short snippets of code that are used only in a limited scope, such as in algorithms, callbacks, or as arguments to other functions.
Lambda functions are part of the C++ syntax itself, and therefore there is no header to include to use this functionality. The basic syntax for a lambda function is as follows:
[capture](parameters) -> return_type {
// lambda function body
};
In the above:
capture
: Specifies which variables from the surrounding scope should be available to the code inside the body of the lambda function. We say these variables from the surrounding scope are captured by the lambda function. We’ll look at an example of this concept in a moment.parameters
: The parameters for the lambda function, similar to those in regular functions.return_type
: (Optional) Specifies the return type of the lambda. If omitted, the return type is deduced automatically.function body
: The code that defines what the lambda does.
We call lambda functions anonymous since the syntax does not specify any function name.
To use the function, you must assign it to a variable,
so you can call the function using that variable’s name.
It is convenient to use the auto
keyword to let the compiler determine the type of the lambda function for you,
similar to how we used it with std::bind
.
Here’s a basic example of a lambda function:
#include <iostream>
int main() {
// A lambda that adds two numbers
auto add = [](int a, int b) -> int {
return a + b;
};
// Using the lambda
std::cout << add(3, 4) << std::endl; // Outputs 7
return 0;
}
In this example we did not “capture” any variables from the surrounding function,
resulting in empty []
brackets,
because this function body only operates on its own two arguments, a
and b
.
In many use cases we do want a lambda function to capture variables from their surrounding scope.
This is specified in the capture clause ([]
).
There are several ways to capture variables:
By value:
[x,y,z]
captures variablesx
,y
,z
by value.By reference:
[&x]
capturesx
by reference.By value for all variables:
[=]
captures all variables used in the lambda by value.By reference for all variables:
[&]
captures all variables used in the lambda by reference.
Here is an example:
#include <iostream>
int main() {
int x = 10;
int y = 20;
// Capture x by value and y by reference
auto f = [x, &y]() {
std::cout << "x: " << x << ", y: " << y << std::endl;
};
f(); // Outputs: x: 10, y: 20
x = 30;
y = 40;
f(); // Outputs: x: 10, y: 40 (!!!)
return 0;
}
Note how x
was captured by value, hence its value within the lambda function remained fixed once the lambda function is defined.
Variable y
was captured by reference, so each time the lambda function is called the current value of y
will be retrieved.
We can also pass lambda functions as a callback to any function that takes std::function
with the correct signature as argument.
Here is an example that behaves similar to the class member function callback before,
but this time the count
message counter is not stored as a class member but as a regular variable on the stack of the main()
function:
#include <iostream>
#include <functional>
void use_callback(const std::function<void(const std::string&)>& callback) {
callback("First time calling callback!");
callback("Second time calling callback!");
callback("Third time calling callback!");
}
int main() {
int count;
// Create a callback using a lambda function
auto print_str = [&count](const std::string& data) -> void {
std::cout << "Message " << count << ": " << data << std::endl;
count++;
};
// Using lambda function as a callback
use_callback(print_str);
return 0;
}
7.3.6. Summary#
The
auto
keyword can be used to simplify writing type declarations in cases where the compiler can figure out the correct type from variable initialization values or function return types (where it can deduce the type automatically).std::bind
andstd::function
are powerful tools introduced in C++11 that allow for greater flexibility in handling functions.std::bind
helps create new function objects by binding some arguments, whilestd::function
acts as a wrapper that can store and call any callable target with a specified signature. Combining these tools can lead to cleaner and more maintainable code, especially in scenarios involving callbacks and event handling.Lambda functions in C++11 provide a concise and powerful way to create anonymous functions. They can capture variables from their surrounding scope, making them highly versatile. When combined with
std::function
and standard algorithms, lambdas enable you to write cleaner and more expressive code.A
std::shared_ptr
is a type of smart pointer that simplifies tracking ownership of data on the heap. Objects managed using shared pointers can be passed around to other functions which accept shared pointers, and thestd::shared_ptr
implementation will count how many of these pointers are still referencing the object. We call this mechanism reference counting. When the last shared pointer is deleted and the objects reference count hits zero, the object will be automatically deleted.