61 / 163 · C++23 · 10 min
C++23 expected and Confirming Availability by Feature
std::expected<T,E> delivers both the success value and the failure reason as a single return type, suited to anticipated failures that the caller must handle. Enabling C++23 mode does not mean the entire standard library is implemented; you should also check the compiler, the standard-library version, and the corresponding feature-test macros.
In this lesson
Success Value and Error Both Become Part of the Interface
C++23's expected<T, E> manages a T on success and an E on failure. It is not syntax that hides exceptions, and it does not handle failure automatically; the caller still needs to check has_value or use the object as a condition. Compared with optional, it retains the failure reason. Compared with an arbitrary variant, it explicitly expresses the two paths of success and error.
The example parses a single digit and returns an int or an enumeration error. Length errors and non-digit errors are expressed separately, and zero remains a legal success value. Using unexpected to construct a failure result makes the failure branch visible at the return site, so the caller does not have to interpret whether some negative integer is data or an error code.
When a function used to return a negative code or an empty optional, those encodings mixed payload with status. expected keeps the channels distinct: a successful zero is still a T, and a parse failure is still an E. That is why both the success value and the error belong in the public interface rather than as a private convention.
Access and Propagation Both Require State Discipline
You may dereference an expected only on success; you may read error only on failure. value provides checked access that throws, but once a design adopts explicit error branches, you should usually read the correct object inside those branches. expected cannot guarantee that constructing or copying T or E never throws, so it does not make the whole function naturally noexcept.
State discipline applies to both reading and propagating the result. A boolean test, then *e or e.error() on the matching side, keeps the preconditions honest. If the design already uses explicit error branches, read the matching object there instead of calling value, which throws.
Monadic operations such as and_then and transform can express continued computation and mapping on the success path, but they have independent library implementation progress. The feature-test macro threshold for basic expected is 202202L; the threshold for monadic operations is 202211L. This lesson uses only the basic interface and does not treat toolchain support for the base type as support for every later member.
A Version Number Is the Entry; Feature Detection Is the Evidence
This site recommends C++20 as the common baseline; the examples in this section need C++23 mode. In a g++ 13.3 environment with its accompanying libstdc++, the basic expected shown here is usable. Different standard-library pairings may change availability. You cannot claim that every C++23 feature is present just from the compiler name or from -std=c++23.
When you actually adopt it, include the corresponding header or version, check whether __cpp_lib_expected reaches the required value, then compile and run verification on the real usage path. Language features and library features should be checked separately. ranges::to, print, generator, and other facilities also have their own implementation progress. Migration should advance by a concrete capability list, not by labeling the entire project as fully supporting C++23 in one step.
A dialect flag selects a language mode; it is not a receipt for the library. Record the macros your code depends on and verify them on each toolchain, including the combination of compiler and standard library you actually ship.
Pitfalls
- Calling error on success or dereferencing on failure both violate state preconditions; expected is not a dynamic object that automatically protects every access.
__cplusplusdescribes the chosen language mode and cannot prove that the standard library provides every feature; basic expected and monadic members should also be distinguished by different macro values.
Run an example
Minimum C++23 · complete program · Download .cpp
#include <cassert>
#include <expected>
#include <iostream>
#include <string_view>
enum class ParseError { wrong_length, not_digit };
std::expected<int, ParseError> parse_digit(std::string_view text) {
if (text.size() != 1) return std::unexpected(ParseError::wrong_length);
const char c = text[0];
if (c < '0' || c > '9') return std::unexpected(ParseError::not_digit);
return c - '0';
}
int main() {
auto good = parse_digit("7");
auto bad = parse_digit("x");
auto empty = parse_digit("");
assert(good && *good == 7);
assert(!bad && bad.error() == ParseError::not_digit);
assert(!empty && empty.error() == ParseError::wrong_length);
std::cout << good.value() << " not_digit\n";
}
Compile locally
g++ -std=c++23 -Wall -Wextra -Wpedantic -pthread modern-cpp23.cpp -o example && ./exampleExpected result
7 not_digit
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If you only want the default value zero on failure, how can you read good and bad? What information is lost?
Show a reference answer
Call good.value_or(0) and bad.value_or(0) respectively; the results are 7 and 0. But a failure result and a successful parse of the character '0' both become zero, and the error reason does not appear in the final number. Adopt a default only when the business truly treats those states as equivalent; otherwise keep the expected and branch explicitly.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.