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 empty std::optional.
  • opt.transform returns a std::optional containing its transformed value or an empty std::optional.
  • opt.or_else returns the std::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.

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • 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.

    • exp.and_then returns the result of the given function call if it exists or an empty std::expected.
    • exp.transform returns a std::expected containing its transformed value or an empty std::exptected.
    • exp.or_else returns the std::expected 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 a std::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)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,