std::expected

std::expected<T, E> holds either a T or an E, and the type says which callers must handle. It is the C++23 answer to the question lesson 061 answered for absence: optional says nothing about why, expected carries the reason.

The tool is for failures that are part of the contract, such as a parse of user input or a lookup that misses. An exception stays for the case the caller cannot be expected to handle at each call, which is what part 7 has covered up to here.

The type is the documentation: a signature of expected<T, E> tells a caller that failure is expected and how it is reported, while a throw inside the body tells them nothing until they read it. For a parser or a lookup, that difference is the whole interface.

A value or an error

The default construction of expected<T, E> holds a value; std::unexpected builds the error side. has_value() asks which one, value() reads the value and error() reads the error.

The program builds one of each and prints the value, both flags and the error message. The two states are part of the type, so a caller cannot ignore them without writing code that does.

value() on the error side throws std::bad_expected_access, as value() on an empty optional throws. The difference is that the error case is usually read with error() or handled by a chain, not thrown.

#include <expected> #include <iostream> #include <string> int main() { std::expected<int, std::string> good = 42; std::expected<int, std::string> bad = std::unexpected{"missing"}; std::cout << good.value() << ' ' << good.has_value() << ' ' << bad.has_value() << ' ' << bad.error() << '\n'; }

g++ -std=c++23 -Wall -Wextra first.cpp -o first
./first

42 1 0 missing

A parse that says why

A function that converts text returns expected<int, std::string>: the number on success and the reason on failure. The caller sees both in the signature, and value_or gives a default in one call.

The program parses a valid and an invalid string and prints the value for the first, the fallback for the second and the error message. The conversion still uses std::stoi, which throws; the throw is caught here and turned into a value.

The try block is the boundary between the exceptions of the library and the value-based interface of the function. Inside this function the two styles meet, and outside it only expected is visible.

#include <expected> #include <iostream> #include <stdexcept> #include <string> std::expected<int, std::string> parse(const std::string& text) { try { return std::stoi(text); } catch (const std::exception&) { return std::unexpected("not a number: " + text); } } int main() { auto good = parse("12"); auto bad = parse("abc"); std::cout << good.value_or(-1) << ' ' << bad.value_or(-1) << ' ' << bad.error() << '\n'; }

g++ -std=c++23 -Wall -Wextra parse.cpp -o parse
./parse

12 -1 not a number: abc

and_then

and_then calls a function only when the expected holds a value, and passes that value on. If the first step failed, the error travels through unchanged and the second function is never called.

The program parses 8 and halves it, then parses 7 and halves it. The first chain returns 4, the second returns the error from half, and neither needs a check between the steps.

transform maps the value without changing the error, and or_else handles the error side. The three functions together are the monadic interface of expected, and they keep a pipeline flat.

#include <expected> #include <iostream> #include <stdexcept> #include <string> std::expected<int, std::string> parse(const std::string& text) { try { return std::stoi(text); } catch (const std::exception&) { return std::unexpected("not a number: " + text); } } std::expected<int, std::string> half(int value) { if (value % 2 != 0) { return std::unexpected("odd number"); } return value / 2; } int main() { auto even = parse("8").and_then(half); auto odd = parse("7").and_then(half); std::cout << even.value_or(-1) << ' ' << odd.error() << '\n'; }

g++ -std=c++23 -Wall -Wextra chain.cpp -o chain
./chain

4 odd number

expected

A function that only reports success or failure uses expected<void, E>. The value side is empty, and the error side is the same as any other expected.

The program's check returns an error for a negative value and an empty value otherwise. The if test on the result reads well, and the error message is available when the check fails.

This is the replacement for a bool plus an output parameter, and for an error code that the caller forgets to read. The type makes the failure impossible to ignore silently.

#include <expected> #include <iostream> #include <string> std::expected<void, std::string> check(int value) { if (value < 0) { return std::unexpected("negative value"); } return {}; } int main() { auto good = check(5); auto bad = check(-1); std::cout << good.has_value() << ' ' << bad.has_value() << ' ' << bad.error() << '\n'; }

g++ -std=c++23 -Wall -Wextra void.cpp -o expected_void
./expected_void

1 0 negative value

An unexpected of the wrong type

The error side of expected<int, std::string> is a std::string, and the constructor only accepts a value it can convert to it. A std::unexpected<int> is not convertible, and the compiler reports the conversion that cannot be done.

The program builds the error from 42 where a string is expected. The message names the two types and points at the initialisation, and the fix is std::string{"42"} or a different error type.

The error type is part of the contract. Changing it changes every caller, which is why a small dedicated error enum is often better than a string for an interface that other code depends on.

#include <expected> #include <string> int main() { std::expected<int, std::string> value = std::unexpected{42}; return value.has_value(); }

g++ -std=c++23 -Wall -Wextra wrong_error.cpp -o wrong_error

wrong_error.cpp: In function 'int main()':
wrong_error.cpp:6:50: error: conversion from 'std::unexpected<int>' to non-scalar type 'std::expected<int, std::__cxx11::basic_string<char> >' requested
6 | std::expected<int, std::string> value = std::unexpected{42};
| ^~~~~~~~~~~~~~

Exercises

  1. Write a function that returns expected for a division and rejects a zero divisor.
  2. Add a transform to the chain that multiplies the halved value by ten.
  3. Change the error type of parse to an enum class and update main.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does std::expected<int, std::string> hold?

    Answer

    An int or a string

    The two types are the value and the reason, and exactly one is stored.

  2. What does has_value() report?

    Answer

    Whether the value or the error is stored

    The call is the check that decides between value() and error().

  3. What does and_then do?

    Answer

    It chains a function that returns another expected

    The chain continues only on the value path, and the error passes through.

  4. What does std::expected<void, E> express?

    Answer

    Success or an error, with no value

    The void specialisation is for operations whose only result is success or a reason.

  5. What does this program print? #include <expected> #include <iostream> #include <string> std::expected<int, std::string> parse(int value) { if (value < 0) return std::unexpected{"negative"}; return value * 2; } int main() { auto good = parse(21); auto bad = parse(-1); std::cout << good.value() << ' ' << good.has_value() << ' ' << bad.has_value() << ' ' << bad.error(); }

    Answer

    42 1 0 negative

    The good parse returns 42, the bad one holds only the message, and has_value tells them apart.

  6. What does this program print? #include <expected> #include <iostream> #include <string> std::expected<int, std::string> parse(const std::string& text) { try { return std::stoi(text); } catch (...) { return std::unexpected{"not a number: " + text}; } } int main() { std::cout << parse("12").value() << ' ' << parse("-1").value_or(-1) << ' ' << parse("abc").error(); }

    Answer

    12 -1 not a number: abc

    The two numbers parse, and the text reports why the third did not.

  7. What does this program print? #include <expected> #include <iostream> #include <string> std::expected<int, std::string> half(int value) { if (value % 2 != 0) return std::unexpected{"odd number"}; return value / 2; } std::expected<int, std::string> chain(int value) { return half(value).and_then([](int v) -> std::expected<int, std::string> { return v; }); } int main() { std::cout << chain(8).value() << ' ' << chain(7).error(); }

    Answer

    4 odd number

    The even value continues through the chain, and the odd one returns its error.

  8. What does this program print? #include <expected> #include <iostream> #include <string> std::expected<void, std::string> check(int value) { if (value < 0) return std::unexpected{"negative value"}; return {}; } int main() { std::cout << check(1).has_value() << ' ' << check(-1).has_value() << ' ' << check(-1).error(); }

    Answer

    1 0 negative value

    The void expected has no value to print, and the error is the string.

  9. What does the compiler report for this line? #include <expected> #include <string> int main() { std::expected<int, std::string> value = std::unexpected{42}; }

    Answer

    conversion from 'std::unexpected<int>' to a non-scalar type is requested

    The error type is int, and the expected stores a string error.

  10. When is expected better than optional?

    Answer

    When the caller needs to know why the value is missing

    optional says nothing about the reason, and expected carries it.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev