C++23: A New Way of Error Handling with std::expected
C++23 extends the interface of std::optional
and gets the new data type std::expected
for error handling.
Before I dive into the extended monadic interface of std::optional
in C++23, I want to introduce this C++17 type.
std::optional
std::optional
is quite comfortable for calculations such as database queries that may have a result. This vocabulary type requires the header <optional
>.
The various constructors and the convenience function std::make_optional
let you define an optional object opt
with or without a value. opt.emplace
will construct the contained value in-place and opt.reset
will destroy the container value. You can explicitly ask a std::optional
container if it has a value, or you can check it in a logical expression. opt.value
returns the value, and opt.value_or
returns the value or a default value. If opt
has no contained value, the call opt.value
will throw a std::bad_optional_access
exception.
Here is a short example using std::optional
.
// optional.cpp #include <optional> #include <iostream> #include <vector> std::optional<int> getFirst(const std::vector<int>& vec){ if ( !vec.empty() ) return std::optional<int>(vec[0]); else return std::optional<int>(); } int main() { std::cout << '\n'; std::vector<int> myVec{1, 2, 3}; std::vector<int> myEmptyVec; auto myInt= getFirst(myVec); if (myInt){ std::cout << "*myInt: " << *myInt << '\n'; std::cout << "myInt.value(): " << myInt.value() << '\n'; std::cout << "myInt.value_or(2017):" << myInt.value_or(2017) << '\n'; } std::cout << '\n'; auto myEmptyInt= getFirst(myEmptyVec); if (!myEmptyInt){ std::cout << "myEmptyInt.value_or(2017):" << myEmptyInt.value_or(2017) << '\n'; } std::cout << '\n'; }
I use std::optional
in the function getFirst
. getFirst
returns the first element if it exists. You get a std::optional
object if not. The main function has two vectors. Both invoke getFirst
and return a std::optional
object. In the case of myInt
, the object has a value; in the case of myEmptyInt
, the object has no value. The program displays the value of myInt
and myEmptyInt
. myInt.value_or(2017)
returns the value, but myEmptyInt.value_or(2017)
returns the default value.
Here is the output of the program.
The Monadic Extension of std::optional
In C++23, std::optional
is extended with monadic operations opt.and_then
, opt.transform
, and opt.or_else.
opt.and_then
returns the result of the given function call if it exists or an emptystd::optional
.opt.transform
returns astd::optional
containing its transformed value or an emptystd::optional
.opt.or_else
returns thestd::optional
if it contains a value or the result of the given function otherwise.
These monadic operations enable the composition of operations on std::optional
:
// optionalMonadic.cpp #include <iostream> #include <optional> #include <vector> #include <string> std::optional<int> getInt(std::string arg) { try { return {std::stoi(arg)}; } catch (...) { return { }; } } int main() { std::cout << '\n'; std::vector<std::optional<std::string>> strings = {"66", "foo", "-5"}; for (auto s: strings) { auto res = s.and_then(getInt) .transform( [](int n) { return n + 100;}) .transform( [](int n) { return std::to_string(n); }) .or_else([] { return std::optional{std::string("Error") }; }); std::cout << *res << ' '; } std::cout << '\n'; }
The range-based for-loop iterates through the std::vector<std::optional<std::string>>
. First, the function getInt
converts each element to an integer, adds 100, converts it back to a string, and finally displays it. If the initial conversion to int
fails, the string Error
is returned and displayed.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
std::expected
already supports the monadic interface.
std::expected
std::expected<T, E>
provides a way to store either of two values. An instance of std::expected
always holds a value: either the expected value of type T
, or the unexpected value of type E
. This vocabulary type requires the header <expected
>. Thanks to std::expected
, you can implement functions that either return a value or an error. The stored value is allocated directly within the storage occupied by the expected object. No dynamic memory allocation takes place.
std::expected
has a similar interface, such as std::optional
. In contrast to std::optional
, std::exptected
can return an error message.
The various constructors let you define an expected object exp
with an expected value. exp.emplace
will construct the contained value in-place. You can explicitly ask a std::expected
container if it has a value, or you can check it in a logical expression. exp.value
returns the expected value, and exp.value_or
returns the expected value or a default value. If exp
has an unexpected value, the call exp.value
will throw a std::bad_expected_access
exception.
std::unexpected
represents the unexpected value stored in std::expected
.
// expected.cpp #include <iostream> #include <expected> #include <vector> #include <string> std::expected<int, std::string> getInt(std::string arg) { try { return std::stoi(arg); } catch (...) { return std::unexpected{std::string(arg + ": Error")}; } } int main() { std::cout << '\n'; std::vector<std::string> strings = {"66", "foo", "-5"}; for (auto s: strings) { // (1) auto res = getInt(s); if (res) { std::cout << res.value() << ' '; // (3) } else { std::cout << res.error() << ' '; // (4) } } std::cout << '\n'; for (auto s: strings) { // (2) auto res = getInt(s); std::cout << res.value_or(2023) << ' '; // (5) } std::cout << '\n'; }
The function getInt
converts each string to an integer and returns a std::expected<int, std::string>
. int
represents the expected, and std::string
the unexpected value. The two range-based for-loops (lines 1 and 2) iterate through the std::vector<std::string>
. In the first range-based for-loop (line 1), the expected (line 3) or the unexpected value (line 4) is displayed. In the second range-based for-loop (line 2), the expected or the default value 2023 (line 5) is displayed.
std::expected
supports monadic operations for convenient function composition: exp.and_then
, exp.transform, exp.or_else
, and exp.transform_error
.
- e
xp.and_then
returns the result of the given function call if it exists or an emptystd::expected
. exp.transform
returns astd::expected
containing its transformed value or an emptystd::exptec
ted.exp.or_else
returns thestd::expecte
d if it contains a value or the result of the given function otherwise.exp.transform_error
returns the std::expected
if it contains an expected value. Otherwise, it returns astd::expected
that contains a transformed unexpected value.
The following program is based on the previous program optionalMonadic.cpp
. Essentially, the type std::optional
is replaced with std::expected
.
// expectedMonadic.cpp #include <iostream> #include <expected> #include <vector> #include <string> std::expected<int, std::string> getInt(std::string arg) { try { return std::stoi(arg); } catch (...) { return std::unexpected{std::string(arg + ": Error")}; } } int main() { std::cout << '\n'; std::vector<std::string> strings = {"66", "foo", "-5"}; for (auto s: strings) { auto res = getInt(s) .transform( [](int n) { return n + 100; }) .transform( [](int n) { return std::to_string(n); }); std::cout << *res << ' '; } std::cout << '\n'; }
The range-based for-loop iterates through the std::vector<std::string>
. First, the function getInt
converts each string to an integer, adds 100
, converts it back to a string, and finally displays it. If the initial conversion to int
fails, the string arg + ": Error"
is returned and displayed.
What’s Next?
The four associative containers std::flat_map
, std::flat_multimap
, std::flat_set
, and std::flat_multiset
in C++23 are a drop-in replacement for the ordered associative containers std::map
, std::multimap
, std::set
, and std::multiset
. We have them for one reason in C++23: performance.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,