Skip to content
C++
Library
since C++23
Basic

std::optional Monadic Operations (C++23)

C++23 and_then, transform, and or_else on std::optional enable composable error-propagation pipelines without nested nullopt checks.

std::optional monadic operationssince C++23

Three member functions β€” and_then, transform, and or_else β€” that enable functional composition over std::optional by threading values through a chain of operations while short-circuiting on nullopt.

Overview

Before C++23, chaining fallible operations through std::optional required either nested if checks or manual helper functions. C++23 (P0798R8) added three monadic operations modelled on the Maybe monad:

  • and_then β€” sequences a failable step: calls f(*opt) if opt holds a value; f must return an optional. Returns empty if the input is empty.
  • transform β€” maps a total function over the value: calls f(*opt) and wraps the result in optional. f returns a plain value, not an optional.
  • or_else β€” provides a fallback when empty: returns *this unchanged if it holds a value, otherwise calls f() (which must return optional<T>).

All three propagate emptiness automatically, so a nullopt at any point in the chain passes through without calling subsequent functions. This is sometimes called railway-oriented programming: the happy path runs straight through; any failure diverts to the empty track.

Each operation provides four ref-qualified overloads (&, const &, &&, const &&) to forward the contained value with the correct value category β€” important when T is move-only or expensive to copy. or_else has only two overloads (const & and &&) because the callable receives no argument.

Syntax

cpp
// and_then β€” f must return optional<U>
template<class F>
constexpr auto and_then(F&& f) &;        // C++23
template<class F>
constexpr auto and_then(F&& f) const &;  // C++23
template<class F>
constexpr auto and_then(F&& f) &&;       // C++23
template<class F>
constexpr auto and_then(F&& f) const &&; // C++23

// transform β€” f returns a plain (non-optional, non-reference, non-void) value
template<class F>
constexpr auto transform(F&& f) &;
template<class F>
constexpr auto transform(F&& f) const &;
template<class F>
constexpr auto transform(F&& f) &&;
template<class F>
constexpr auto transform(F&& f) const &&;

// or_else β€” f takes no arguments, must return optional<T>
template<class F>
constexpr optional or_else(F&& f) const &;
template<class F>
constexpr optional or_else(F&& f) &&;

The return type of and_then is remove_cvref_t<invoke_result_t<F, T&>> β€” whatever f returns. The return type of transform is optional<remove_cv_t<invoke_result_t<F, T&>>>.

Examples

Parsing pipeline

cpp
#include <optional>
#include <charconv>
#include <cmath>
#include <string_view>

std::optional<int> parse_int(std::string_view sv) {          // C++17
    int result{};
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), result);
    return ec == std::errc{} ? std::optional{result} : std::nullopt;
}

std::optional<double> safe_sqrt(int n) {                      // C++17
    return n >= 0 ? std::optional{std::sqrt(static_cast<double>(n))} : std::nullopt;
}

// C++23: chain parse β†’ validate β†’ sqrt
auto r1 = std::optional<std::string_view>{"16"}
    .and_then(parse_int)    // optional{16}
    .and_then(safe_sqrt);   // optional{4.0}

auto r2 = std::optional<std::string_view>{"abc"}
    .and_then(parse_int)    // nullopt β€” short-circuits
    .and_then(safe_sqrt);   // never called; r2 == nullopt

Transforming without introducing optionality

cpp
// transform: f returns T, not optional<T>
auto upper_first = std::optional<std::string>{"hello"}
    .transform([](std::string s) -> char {
        return static_cast<char>(std::toupper(static_cast<unsigned char>(s[0])));
    });
// upper_first == optional{'H'}

// Chain transforms β€” each is a total function
auto formatted = std::optional{42}
    .transform([](int n) { return n * 2; })            // optional{84}
    .transform([](int n) { return std::to_string(n); }) // optional{"84"}
    .transform([](std::string s) { return "[" + s + "]"; }); // optional{"[84]"}

Fallback chains with or_else

cpp
struct Config { std::string path; };

std::optional<Config> load_from_env();
std::optional<Config> load_from_file(std::string_view path);
std::optional<Config> default_config();

auto config = load_from_env()
    .or_else([] { return load_from_file("/etc/myapp/config.toml"); })
    .or_else([] { return load_from_file("~/.myapp"); })
    .or_else(default_config);
// config is guaranteed to hold a value if default_config() always returns one

Real-world pipeline: request handling

cpp
struct Request  { int user_id; std::string body; };
struct User     { int id; bool active; std::string email; };
struct Profile  { std::string display_name; std::string avatar_url; };

std::optional<User>    fetch_user(int id);
std::optional<Profile> fetch_profile(const User& u);

// Only active users have profiles exposed
std::optional<Profile> get_active_profile(const Request& req) {
    return fetch_user(req.user_id)
        .and_then([](const User& u) -> std::optional<User> {
            return u.active ? std::optional{u} : std::nullopt;
        })
        .and_then(fetch_profile);
}

// Without monadic chaining β€” equivalent but noisier
std::optional<Profile> get_active_profile_old(const Request& req) {
    auto user = fetch_user(req.user_id);
    if (!user || !user->active) return std::nullopt;
    return fetch_profile(*user);
}

Best Practices

Prefer transform over and_then for total functions. If f cannot fail, use transform β€” it communicates intent and avoids the accidental double-wrapping that and_then would require from returning optional<optional<T>>.

Use or_else instead of value_or for lazy fallbacks. value_or(expr) evaluates expr unconditionally; or_else(f) calls f only when the optional is empty. For any fallback that requires a function call or allocation, or_else is strictly better.

cpp
// Eager β€” default_widget() always constructed:
auto w = find_widget(id).value_or(default_widget());

// Lazy β€” default_widget() called only when needed:
auto w = find_widget(id)
    .or_else([] { return std::optional{default_widget()}; })
    .value();  // safe here if or_else guarantees non-empty

Anchor value extraction at the end. Call value_or, value, or a final transform after the chain resolves, not in the middle. Extracting early with *opt bypasses the monadic interface and forces you back to manual checks.

Common Pitfalls

and_then with a function returning T instead of optional<T> β€” this is a hard compile error, but the message can be opaque. If you get a concept-constraint failure mentioning is-optional-like, the callable passed to and_then is returning the wrong type.

cpp
// Wrong: parse_int returns optional<int>, not int
auto r = opt.and_then([](int n) { return n * 2; });  // ill-formed

// Correct: use transform for total functions
auto r = opt.transform([](int n) { return n * 2; });

transform with a callable returning optional<T> β€” this compiles but produces optional<optional<T>>, almost certainly not the intended type.

cpp
std::optional<int> clamp_positive(int n);  // returns optional

// Produces optional<optional<int>>, not optional<int>:
auto r = std::optional{-5}.transform(clamp_positive);

// Use and_then instead:
auto r = std::optional{-5}.and_then(clamp_positive);

or_else callable returning T instead of optional<T> β€” also a constraint error. The fallback must return the same optional<T> specialization, not a bare T.

cpp
// Wrong:
auto r = opt.or_else([] { return 42; });  // ill-formed

// Correct:
auto r = opt.or_else([] { return std::optional{42}; });

Chaining on temporaries with move-only types. The &&-qualified overloads move the contained value into the callable, so and_then on an rvalue optional correctly propagates ownership. If you call and_then on an lvalue optional holding a unique_ptr, the callable receives an lvalue reference β€” not a move β€” which is correct but can surprise readers expecting ownership transfer.

See Also