How optional and error values are returned in iceoryx🔗
Many parts of the iceoryx C++ API follow a functional programming approach and allow the user to specify functions which handle the possible cases, e.g. what should happen when data is received.
This is very flexible but requires using the monadic types iox::cxx::expected
and iox::cxx::optional
, which we
introduce in the following sections.
Optional🔗
The type iox::cxx::optional<T>
is used to indicate that there may or may not be a value of a specific type T
available. This is essentially the 'maybe monad' in
functional programming. Assuming we have some optional (usually the result of some computation)
optional<int> result = someComputation();
we can check for its value using
if(result.has_value())
{
auto value = result.value();
// do something with the value
}
else
{
// handle the case that there is no value
}
A shorthand to get the value is
auto value = *result;
Attention
Accessing the value if there is no value terminates the application, so it must be checked beforehand.
We can achieve the same with the functional approach by providing a function for both cases.
result.and_then([](int& value) { /*do something with the value*/ })
.or_else([]() { /*handle the case that there is no value*/ });
Notice that we get the value by reference, so if a copy is desired it has to be created explicitly in the lambda expression or function we pass.
The optional can be initialized from a value directly
optional<int> result = 73;
result = 37;
If the optional is default initialized, it is automatically set to its null value of type iox::cxx::nullopt_t
.
This can be also done directly by using the constant iox::cxx::nullopt
result = iox::cxx::nullopt;
For a complete list of available functions see
optional.hpp
.
The iox::cxx::optional
behaves like the std::optional
except that it does not throw exceptions and has no undefined behavior.
Expected🔗
iox::cxx::expected<T, E>
generalizes iox::cxx::optional
by admitting a value of another type E
instead of
no value at all, i.e. it contains either a value of type T
or E
. In this way, expected
is a special case of
the 'either monad'. It is usually used to pass a value of type T
or an error that may have occurred, i.e. E
is the
error type.
For more information on how it is used for error handling see error-handling.md.
Assume we have E
as an error type, then we can create a value
iox::cxx::expected<int, E> result(iox::cxx::success<int>(73));
and use the value or handle a potential error
if (!result.has_error())
{
auto value = result.value();
// do something with the value
}
else
{
auto error = result.get_error();
// handle the error
}
If we need an error value, we set
result = iox::cxx::error<E>(errorCode);
which assumes that E
can be constructed from an errorCode
.
We again can employ a functional approach like this:
auto handleValue = [](int& value) { /*do something with the value*/ };
auto handleError = [](E& value) { /*handle the error*/ };
result.and_then(handleValue).or_else(handleError);
There are more convenience functions such as value_or
which provides the value or an alternative specified by the
user. These can be found in
expected.hpp
.
Note that when we move an expected
, the origin contains a moved T
or E
, depending on the content before the move.
This reflects the behavior of moving the content out of the iox::cxx::expected
as in
auto foo = std::move(bar.value());
with bar
being an iox::cxx::expected
.
Like all objects, T
and E
must therefore be in a well defined state after the move.