C++23: A New Way of Error Handling with std::expected
This post is a cross-post from www.ModernesCpp.com.
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.
?
Modernes C++ Mentoring
Be part of my mentoring programs:
"C++20: Get the Details" (start: 11 August 2023)
Stay informed: Subscribe via E-Mail.
?
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.
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.
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, Kris Kafka, 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, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, 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, and Bhavith C Achar.
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, PVS-Studio , Tipi.build, and Take Up Code.
Seminar
I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.
Bookable
German
Standard Seminars (English/German)
Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.
New
Contact Me
Modernes C++ Mentoring,