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

std::optional

A value-or-nothing wrapper that eliminates sentinel values and nullable pointer workarounds in C++17.

std::optional<T>since C++17

A vocabulary type that holds either a value of type T or an empty state (std::nullopt), stored inline without heap allocation, providing explicit nullability with value semantics.

Overview

std::optional<T> solves the problem of functions that may or may not produce a result. Before C++17, the idioms for this were all compromised: returning a pointer (forces heap allocation, obscures ownership), using sentinel values like -1 or "" (fragile, domain-dependent), or output parameters with a bool return (awkward call sites). std::optional makes the optionality part of the type.

This is a vocabulary type β€” a type whose presence in a function signature communicates intent. std::optional<User> find_user(int id) tells the caller immediately that the lookup may fail, and the type system enforces checking.

When to use std::optional:

  • Function return values that may legitimately have no result
  • Optional function parameters (replacing overloads or nullable pointers)
  • Class members that are conditionally present

When not to use it:

  • As a lazy substitute for error handling β€” if failure has a reason, prefer std::expected<T, E> (C++23) or exceptions
  • To wrap raw pointers β€” optional<T*> adds an unnecessary empty state; nullptr already encodes absence
  • When you need reference semantics β€” optional<T&> is ill-formed; use optional<reference_wrapper<T>>

Syntax

cpp
// Construction
std::optional<T> opt;                         // empty
std::optional<T> opt = std::nullopt;          // empty, explicit
std::optional<T> opt = value;                 // engaged, copy/move from value
std::optional<T> opt{std::in_place, args...}; // construct T in-place, C++17
auto opt = std::make_optional<T>(args...);    // helper, C++17

// Observers
bool engaged = opt.has_value(); // or: if (opt)
T&   val     = *opt;            // UB if empty β€” no check
T&   val     = opt.value();     // throws std::bad_optional_access if empty
T    val     = opt.value_or(d); // returns d if empty
T*   ptr     = opt.operator->(); // direct member access

// Modifiers
opt = new_value;          // assign
opt.emplace(args...);     // construct in-place, destroys existing value
opt.reset();              // clear to empty
opt = std::nullopt;       // same as reset()

// C++23 monadic interface
opt.and_then(f)           // f(T) -> optional<U>; propagates empty
opt.transform(f)          // f(T) -> U; wraps result in optional<U>
opt.or_else(f)            // f() -> optional<T>; called only when empty

Examples

Parsing and chaining results

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

std::optional<int> parse_int(std::string_view s) {
    int result{};
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), result);
    if (ec != std::errc{} || ptr != s.data() + s.size())
        return std::nullopt;
    return result;
}

std::optional<int> positive_only(int n) {
    return n > 0 ? std::optional{n} : std::nullopt;
}

// C++23: monadic chaining β€” each step short-circuits on empty
auto result = parse_int("42")
    .and_then(positive_only)
    .transform([](int n) { return n * 2; });

// result is optional<int> containing 84
// parse_int("-1") -> positive_only -> short-circuits -> nullopt
// parse_int("abc") -> nullopt immediately

Optional function parameters

cpp
// Replace two overloads with one function
void connect(std::string_view host,
             std::optional<int> port = std::nullopt,
             std::optional<std::string_view> user = std::nullopt) {
    int p = port.value_or(5432);
    // ...
}

connect("localhost");
connect("localhost", 5433);
connect("localhost", std::nullopt, "admin");

Optional class members

cpp
struct Config {
    std::string host;
    int port{5432};
    std::optional<std::string> password;  // not always required
    std::optional<std::chrono::seconds> timeout;
};

Config cfg{"db.example.com"};
if (cfg.password) {
    authenticate(*cfg.password);
}
auto deadline = cfg.timeout.value_or(std::chrono::seconds{30});

Building a lookup that chains

cpp
#include <unordered_map>

using Registry = std::unordered_map<std::string, std::string>;

std::optional<std::string> lookup(const Registry& r, const std::string& key) {
    auto it = r.find(key);
    return it != r.end() ? std::optional{it->second} : std::nullopt;
}

// Resolve an alias chain (A -> B -> C)
std::optional<std::string> resolve(const Registry& aliases, std::string key) {
    while (auto next = lookup(aliases, key))
        key = *next;
    return key.empty() ? std::nullopt : std::optional{key};
}

emplace for non-copyable or expensive types

cpp
std::optional<std::vector<int>> build_data(bool condition) {
    std::optional<std::vector<int>> result;
    if (condition)
        result.emplace(1'000'000, 0);  // constructs in-place, no copy
    return result;
}

Construction and Emplacement

std::optional offers several ways to construct its contained value, each with different trade-offs.

std::make_optional<T>(args...) is the most convenient form when you want CTAD or need to construct from forwarded arguments. std::optional<T>{std::in_place, args...} constructs T directly in the optional's storage without an intermediate object β€” essential when T is not moveable or when the move is expensive. Both forms have the same observable behavior for well-behaved types.

cpp
#include <optional>
#include <vector>
#include <string>

// make_optional: deduces T, constructs and wraps
auto o1 = std::make_optional<std::string>(5, 'x'); // optional{"xxxxx"}  // C++17

// in_place: constructs directly in storage β€” no temporary, no move
std::optional<std::string> o2{std::in_place, 5, 'x'};                    // C++17

// emplace: constructs in-place after default-construction,
// destroying any existing value first
std::optional<std::vector<int>> opt;
opt.emplace(5, 0);  // constructs vector{0, 0, 0, 0, 0} in-place
                    // no temporary vector created, no move needed

emplace(args...) is particularly useful in loops or conditional branches where the optional starts empty and is later populated with an expensive-to-move type. It also returns a reference to the newly constructed value, allowing immediate use:

cpp
auto& v = opt.emplace(10, 42);   // constructs vector{42,...} (10 elements)
v.push_back(99);                  // use the reference directly

Best Practice. Prefer emplace over opt = T{args...} when constructing complex objects. The assignment form constructs a temporary T, then move-assigns it, calling the destructor on any prior value. emplace eliminates the temporary and the extra destructor call.

Monadic Operations (C++23)

C++23 (P0798) added three member functions that allow composing fallible operations in a pipeline without repeated if (!opt) guards. Each operation propagates emptiness automatically: a nullopt at any step passes through without calling subsequent functions.

  • transform(f) β€” applies a total (always-succeeding) function to the value; f returns U, the result is optional<U>.
  • and_then(f) β€” sequences a failable step; f returns optional<U> and the result is flattened to optional<U>.
  • or_else(f) β€” provides a fallback when the optional is empty; f takes no arguments and returns optional<T>.

A realistic pipeline combining all three:

cpp
#include <optional>
#include <string>
#include <filesystem>
#include <fstream>

// Each step may fail β€” returns optional                                   // C++23
std::optional<std::filesystem::path> find_config_path();
std::optional<std::string>           read_file(std::filesystem::path p);
std::optional<Config>                parse_json(const std::string& text);
std::optional<Config>                default_config();

auto config = find_config_path()
    .and_then(read_file)      // path -> file contents; nullopt if unreadable
    .and_then(parse_json)     // text -> Config; nullopt if malformed JSON
    .or_else(default_config); // if anything above failed, use defaults

transform maps without introducing optionality; and_then flattens the result of a step that itself returns an optional; or_else recovers at the end of a chain.

cpp
// transform: f returns T, not optional<T>
auto doubled = std::optional{21}
    .transform([](int n) { return n * 2; });   // optional{42}   // C++23

// and_then: f returns optional<U> β€” avoids double-wrapping
std::optional<int> safe_div(int n) {
    return n != 0 ? std::optional{100 / n} : std::nullopt;
}
auto r = std::optional{5}.and_then(safe_div);  // optional{20}   // C++23

// or_else: fallback when empty β€” called lazily, only on nullopt
auto val = std::optional<int>{}
    .or_else([] { return std::optional{42}; }); // optional{42}  // C++23

For a full treatment of these operations, including ref-qualified overloads, pitfalls (transform accidentally producing optional<optional<T>>), and extended pipeline examples, see the companion page std::optional Monadic Operations.

optional<T&> Workaround

std::optional<T&> is ill-formed β€” the standard explicitly prohibits reference types as T. This is intentional: the assignment semantics for optional references are ambiguous (should assignment rebind the reference or assign through it?).

The practical workaround is std::optional<std::reference_wrapper<T>>. std::reference_wrapper<T> is a copyable, assignable handle to a T&, constructed with std::ref() and accessed via .get().

cpp
#include <optional>
#include <functional>

int x = 42;
int y = 99;

// Construct: std::ref wraps x by address
std::optional<std::reference_wrapper<int>> opt = std::ref(x); // C++17

// Check and access
if (opt) {
    opt->get() = 100;  // modifies x directly
}
// x is now 100

// Reassign the wrapper to point at y
opt = std::ref(y);     // opt now refers to y β€” rebinds the wrapper
opt->get() = 200;      // modifies y directly
// y is now 200

// Empty state still works normally
opt.reset();           // now empty β€” refers to nothing

The main awkwardness is the .get() call required to access the value. A thin alias hides the noise at the declaration site:

cpp
template<class T>
using optional_ref = std::optional<std::reference_wrapper<T>>;

optional_ref<int> find_even(std::vector<int>& v) {
    for (int& n : v)
        if (n % 2 == 0) return std::ref(n);
    return std::nullopt;
}

std::vector<int> nums{1, 3, 4, 7};
if (auto r = find_even(nums))
    r->get() *= 10;   // nums is now {1, 3, 40, 7}

A future standard revision may add optional<T&> directly (proposed in P2988). Until then, optional<reference_wrapper<T>> is the idiomatic solution.

Comparison Semantics

std::optional has a total order. Two optionals compare according to three rules:

  1. Both empty β†’ equal.
  2. One empty, one engaged β†’ the empty one is less than the engaged one.
  3. Both engaged β†’ compare their contained values using T's operators.
cpp
#include <optional>

std::optional<int> none;
std::optional<int> five{5};
std::optional<int> ten{10};

none < five   // true  β€” empty is less than any engaged value   // C++17
five < ten    // true  β€” compares contained values: 5 < 10
none == none  // true  β€” both empty
five == five  // true  β€” both engaged with equal values
ten  > five   // true  β€” 10 > 5

Comparison with std::nullopt directly is also supported. opt > std::nullopt is true if and only if opt has a value; opt == std::nullopt is true if and only if opt is empty.

cpp
std::optional<int> opt{42};
opt > std::nullopt    // true  β€” opt has a value         // C++17
opt == std::nullopt   // false β€” opt is not empty

std::optional<int> empty;
empty == std::nullopt // true  β€” empty is empty
empty < std::nullopt  // false β€” empty == nullopt, not less

Best Practice. Rely on the default comparison rather than manually checking has_value() and then comparing *opt. The overloaded operators handle the asymmetric empty/engaged cases correctly, and they compose with standard algorithms like std::sort and std::min.

value_or and Assignment

value_or(default) is the workhorse accessor for optionals in non-throwing code. It returns the contained value if present, otherwise returns the argument β€” without any exception risk.

cpp
std::optional<std::string> name;
std::string display = name.value_or("anonymous");  // "anonymous"   // C++17

name = "Alice";
display = name.value_or("anonymous");              // "Alice"

Note that value_or returns by value (not reference), so it always copies or moves. Avoid passing it a default that is expensive to construct when the optional is usually engaged β€” use a lazy or_else (C++23) instead.

Assignment covers three natural forms:

cpp
std::optional<int> opt;

opt = 42;            // assigns (constructs if empty)     // C++17
opt = std::nullopt;  // resets to empty (same as opt.reset())
opt = {};            // also resets β€” {} is nullopt for optional

std::optional<int> other{99};
opt = other;         // copy-assigns from another optional
                     // if other is empty, opt becomes empty too

Assigning from another optional of the same type follows value semantics: if other is engaged, opt becomes engaged with the same value; if other is empty, opt becomes empty. This is the expected behavior but can surprise code that expects assignment to always produce an engaged optional.

optional as Function Parameter

Using std::optional<T> as a function parameter by value is a common anti-pattern on hot paths. When the caller passes a std::string, the compiler must construct an optional<string> β€” potentially copying the string into the optional's storage β€” before the call begins. For a parameter that is almost always present, this overhead is unjustified.

cpp
// Anti-pattern for hot paths: forces construction of optional<string>    // C++17
void log_message(std::optional<std::string> tag, std::string_view msg);

log_message("network", "connected");  // "network" copied into optional<string>

Better alternatives for hot paths:

cpp
// Option 1: raw pointer β€” null encodes absence, no wrapping overhead
void log_message(const std::string* tag, std::string_view msg);
log_message(&tag_str, "connected");
log_message(nullptr,  "connected");

// Option 2: overloads β€” zero overhead at each call site
void log_message(std::string_view msg);
void log_message(std::string_view tag, std::string_view msg);

// Option 3: string_view β€” a view cannot own, so absent = empty view
void log_message(std::string_view tag, std::string_view msg);
log_message("",   "connected");  // empty view signals absence

Optional parameters are fine β€” and idiomatic β€” when:

  • The function is not on a hot path (config, initialization, one-shot operations).
  • The type T is cheap to copy (integral types, string_view, small structs).
  • The parameter is genuinely optional in the domain model (e.g., a config struct field).
cpp
// Fine: config struct, cold path                                          // C++17
struct ServerOptions {
    std::string host;
    int port{8080};
    std::optional<std::string> tls_cert;      // legitimately optional
    std::optional<std::chrono::seconds> timeout;
};

The rule of thumb: if the parameter is absent in more than ~20% of calls or T involves allocation, prefer a raw pointer or overloads; otherwise optional is fine.

std::optional<std::optional<T>>

A double-optional is legal and occasionally genuinely useful. It has two distinct empty states:

  • Outer empty (nullopt) β€” the optional itself was never populated.
  • Outer engaged, inner empty (optional{nullopt}) β€” the optional was populated, but the result was absent.

This models a three-state "not yet loaded / loaded but absent / loaded and present" distinction that a single optional cannot express.

cpp
#include <optional>

// Three distinct states:
std::optional<std::optional<User>> cache;

// State 1: not yet fetched from DB (outer empty)
cache = std::nullopt;

// State 2: fetched, but user does not exist (outer engaged, inner empty)
cache = std::optional<User>{std::nullopt};

// State 3: fetched, user exists
cache = std::optional<User>{User{42, "alice"}};

// Distinguish states
if (!cache.has_value()) {
    // Case 1: needs a fetch
} else if (!cache->has_value()) {
    // Case 2: confirmed absent β€” don't re-fetch
} else {
    // Case 3: use **cache
    const User& user = **cache;
}

Common Pitfall. The Pitfall section below notes that a naΓ―ve optional<optional<T>> "loses information" β€” that is true if you collapse the two levels together. The point of a double-optional is precisely to keep those levels distinct. If you find yourself checking opt.has_value() && !opt->has_value() routinely, a named enum or std::variant<std::monostate, std::nullopt_t, User> may be clearer to readers.

cpp
// Alternative: explicit three-state enum
enum class LoadState { Pending, Absent, Present };
struct UserCache {
    LoadState state = LoadState::Pending;
    std::optional<User> value;
};

Use the double-optional when the "not yet loaded" vs "absent" distinction is already implicit in the optional's presence in a cache or map. Use an explicit struct or variant when readers need to reason about all three states simultaneously.

Best Practices

Prefer value_or over manual checking for defaults. It expresses intent inline and reads cleanly at call sites.

cpp
// Verbose
std::string name = opt_name.has_value() ? *opt_name : "anonymous";
// Clear
std::string name = opt_name.value_or("anonymous");

Use emplace when constructing complex objects. opt = T{args...} constructs a T then move-assigns; opt.emplace(args...) constructs directly in the optional's storage, saving the move and the destructor call on any prior value.

Prefer and_then/transform/or_else (C++23) for chaining. They eliminate the pattern of repeatedly checking if (opt) and manually threading the value through, and they compose better in lambdas.

Don't return optional for errors with reasons. If a lookup fails because the database is unreachable versus because the key doesn't exist, those are different outcomes. Use std::expected<T, E> (C++23) or exceptions for richer diagnostics.

std::nullopt in return is idiomatic; {} also works. Both return std::nullopt; and return {}; produce an empty optional from a function returning optional<T>. std::nullopt is more explicit about intent.

Common Pitfalls

operator* on an empty optional is UB

cpp
std::optional<int> opt;
int x = *opt;  // undefined behavior β€” no exception, no check

Use opt.value() if you want std::bad_optional_access on empty access. Use *opt only after you've verified opt.has_value() or tested if (opt).

optional<bool> surprising truthiness

cpp
std::optional<bool> flag = false;
if (flag) {
    // This branch IS taken β€” flag is engaged (has a value),
    // even though *flag == false
}
// Test the value, not the optional:
if (flag && *flag) { /* value is true */ }
if (flag.value_or(false)) { /* empty treated as false */ }

optional<optional<T>> loses information

cpp
// Collapsing: you can't distinguish "outer empty" from "inner empty"
std::optional<std::optional<int>> weird;
// Use a tri-state enum, std::variant<std::monostate, int, Error>, or similar
// unless you explicitly need two distinct empty states (see section above)

optional<T*> is almost never right

cpp
// Two ways to represent "nothing": nullopt and nullptr β€” confusing
std::optional<int*> p;    // don't do this
int* p = nullptr;         // fine: pointer already encodes absence
std::optional<int> val;   // use optional for value types

Dangling reference from value_or with temporaries

cpp
const std::string& get_name(const std::optional<std::string>& opt) {
    // UB if opt is empty: temporary string destroyed at semicolon
    return opt.value_or("default");
}
// value_or returns T, not T&. Bind by value, not reference.

Performance

std::optional<T> stores T inline in its own storage β€” no heap allocation. The engaged state is tracked with an additional bool, so sizeof(optional<T>) is typically sizeof(T) aligned up, plus one byte, rounded to alignment.

cpp
#include <optional>
#include <cstdio>

// Concrete sizes on a 64-bit platform:
static_assert(sizeof(std::optional<int>)    == 8);  // 4-byte int + 1-byte flag + 3 padding
static_assert(sizeof(std::optional<double>) == 16); // 8-byte double + 1-byte flag + 7 padding
static_assert(sizeof(std::optional<char>)   == 2);  // 1-byte char + 1-byte flag, no padding

// Verify at runtime
std::printf("optional<int>    = %zu bytes\n", sizeof(std::optional<int>));
std::printf("optional<double> = %zu bytes\n", sizeof(std::optional<double>));

For small types like int, char, or std::string_view, the overhead is negligible β€” usually a single cache line either way. For polymorphic types where T is a large object or involves virtual dispatch, consider std::optional<std::unique_ptr<T>> instead: it trades in-place storage for pointer indirection but avoids copying the object.

Copy and move costs mirror those of T itself. An empty optional copies or moves in O(1) regardless of T. The compiler can often eliminate the bool check entirely when the engaged state is statically known.

Common Pitfall. Padding can make optional<T> noticeably larger than T alone when T has strict alignment requirements. A double (8-byte aligned) inside an optional jumps from 8 bytes to 16 because the single bool flag forces 7 bytes of trailing padding. If you store many such optionals in a struct, consider grouping them at the end of the struct or using a bitfield to pack flags manually.

See Also

  • std::variant (C++17) β€” holds exactly one of several types; use when there are multiple distinct "something" states
  • std::expected<T, E> (C++23) β€” like optional but carries an error value on failure; prefer this when failure has a reason
  • std::any (C++17) β€” type-erased container for a single value of any copyable type; trades compile-time type safety for runtime flexibility
  • std::reference_wrapper<T> β€” wraps a reference in a copyable/assignable object; combine with optional when you need optional references
  • std::optional Monadic Operations β€” deep dive into and_then, transform, and or_else with extended pipeline examples (C++23)