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

std::optional: Express 'No Value' Without Pointers or Sentinel Values

Learn to use std::optional to represent values that may or may not be present — eliminating null pointers, magic sentinel values, and out-parameters from C++ APIs.

By the end of this page you will return std::optional<T> from functions that may not produce a value, safely access the contained value, chain operations on optionals with monadic methods (C++23), and avoid the three mistakes that cause undefined behaviour.

What and Why

Before C++17, "this function might not return a value" was expressed in several fragile ways:

PatternProblem
Return a raw pointer, null on failureWho owns it? Is null a valid value?
Return a magic sentinel (-1, "")What if the sentinel is a valid value?
bool return + out-parameterVerbose; can silently ignore the bool
Throw on "no value"Exceptions are expensive and not always appropriate

std::optional<T> (C++17, <optional>) is a vocabulary type that holds either a T or nothing. The absence of a value is explicit and type-enforced:

cpp
#include <optional>
#include <string>

std::optional<std::string> find_user(int id) {
    if (id == 42) return "Alice";
    return std::nullopt;   // explicit "no value"
}

int main() {
    if (auto user = find_user(42)) {
        std::cout << *user << '\n';   // "Alice"
    }
    if (auto user = find_user(99)) {
        // this block is skipped — user is empty
    }
}

optional<T> stores the value inline (no heap allocation). It is exactly sizeof(T) + 1 byte in practice (alignment may add more).

Step by Step

Creating and returning

cpp
#include <optional>

// Return a value
std::optional<int> parse_int(const std::string& s) {
    try   { return std::stoi(s); }
    catch (...) { return std::nullopt; }
}

// Construct in-place without a temporary
std::optional<std::vector<int>> make_vec(bool fill) {
    if (!fill) return std::nullopt;
    return std::vector<int>{1, 2, 3};   // move-constructed into optional
}

// Explicit emplace: construct in place, avoiding a copy/move
std::optional<ExpensiveType> opt;
opt.emplace(arg1, arg2);  // constructs ExpensiveType(arg1, arg2) in place

Checking and accessing

cpp
std::optional<int> opt = parse_int("42");

// Check
if (opt) { /* has value */ }
opt.has_value()   // same as (bool)opt

// Dereference — UB if empty!
*opt              // int&
opt.value()       // int& — throws std::bad_optional_access if empty (safer)
opt.value_or(0)   // returns *opt if present, 0 otherwise (no throw)

// Arrow operator for member access
std::optional<std::string> s = "hello";
s->size()         // 5

Resetting

cpp
std::optional<int> opt = 42;
opt.reset();       // now empty
opt = std::nullopt; // equivalent

Monadic operations (C++23)

C++23 adds three chainable methods that let you transform or recover from an empty optional without explicit if checks:

cpp
#include <optional>
#include <string>

std::optional<int> parse_int(std::string_view s);
std::optional<std::string> lookup_name(int id);

// and_then: chain a function that returns optional — short-circuits on empty
auto result = parse_int(input)
    .and_then(lookup_name);   // returns std::optional<std::string>

// transform: apply a function to the value, wraps result in optional
auto doubled = parse_int("21")
    .transform([](int x) { return x * 2; });  // optional<int>{42}

// or_else: provide a fallback optional when empty
auto with_default = parse_int(bad_input)
    .or_else([] { return std::optional<int>{0}; });

These compose without nesting if checks or intermediate variables.

Common Patterns

Pattern 1 — Replace bool + out-parameter

cpp
// Old C style
bool find_config(const std::string& key, std::string& out);

// Modern
std::optional<std::string> find_config(const std::string& key);

// Usage is cleaner
if (auto cfg = find_config("timeout")) {
    apply(*cfg);
}

Pattern 2 — Lazy initialization

cpp
struct Cache {
    mutable std::optional<ComputedResult> cached_;

    const ComputedResult& get() const {
        if (!cached_)
            cached_.emplace(compute());   // constructed on first call
        return *cached_;
    }
};

Pattern 3 — Optional function parameters

cpp
void connect(std::string_view host,
             int port,
             std::optional<int> timeout_ms = std::nullopt) {
    int ms = timeout_ms.value_or(5000);
    // ...
}

connect("localhost", 8080);            // uses default timeout
connect("remote.host", 443, 1000);    // explicit timeout

This is cleaner than overloading because the intent is explicit and a single definition covers all cases.

Pattern 4 — Comparison and equality

optional<T> supports all comparison operators. Two optionals compare their contained values when both are non-empty; an empty optional is always less than any non-empty optional:

cpp
std::optional<int> a = 1;
std::optional<int> b = 2;
std::optional<int> empty;

a < b        // true — compares 1 < 2
empty < a    // true — empty is always less than any value
a == 1       // true — compare directly with the contained type
a != std::nullopt  // true — a has a value
empty == std::nullopt  // true — both represent "no value"

optional also compares directly with T and std::nullopt, making it natural to use in sorted containers and standard algorithms that rely on ordering.

Pattern 5 — optional in standard algorithms

Because optional<T> converts to bool, it works naturally with any algorithm that takes a predicate:

cpp
#include <optional>
#include <vector>
#include <algorithm>
#include <ranges>

std::vector<std::optional<int>> data = {1, std::nullopt, 3, std::nullopt, 5};

// Count non-empty entries
auto count = std::count_if(data.begin(), data.end(),
    [](const auto& o) { return o.has_value(); });  // 3

// Collect only the present values (C++23 ranges pipeline)
auto values = data
    | std::views::filter([](const auto& o) { return o.has_value(); })
    | std::views::transform([](const auto& o) { return *o; });
// values lazily yields: 1, 3, 5

The filter-then-transform pattern above is idiomatic for "unwrap all non-empty optionals from a sequence."

Performance note: sizeof(optional)

std::optional<T> stores its value inline — no heap allocation ever occurs. The layout is roughly one T plus one byte for the "has value" flag, rounded up to alignment:

TypeTypical sizeof
optional<bool>2 bytes (1 for bool, 1 flag)
optional<int>8 bytes (4 for int, 1 flag, 3 padding)
optional<double>16 bytes (8 for double, 1 flag, 7 padding)
optional<std::string>sizeof(string) + alignment

The inline storage means copying or moving an optional<T> is exactly as expensive as copying or moving a T. For types that are large and rarely present, the stack cost may be undesirable — in those cases prefer std::unique_ptr<T> (heap-allocated, nullable) over optional<T> (always occupies stack space).

What Can Go Wrong

Dereferencing an empty optional

cpp
std::optional<int> opt;
int x = *opt;          // UB: undefined behaviour
int y = opt.value();   // throws std::bad_optional_access — safer

Always check .has_value() or use value_or() before dereferencing.

Using optional<T&> — it doesn't exist

std::optional cannot hold reference types. Use a raw pointer or std::reference_wrapper for optional references:

cpp
// Won't compile
std::optional<int&> opt_ref;   // error: references not allowed

// Alternatives
int* ptr = condition ? &value : nullptr;       // raw pointer
std::optional<std::reference_wrapper<int>> rw; // reference_wrapper

optional does not catch exceptions

std::optional signals "no value for this call" — not "this call failed with an error." For error cases that carry a reason, use std::expected<T, E> (C++23) instead.

Performance: optional of large types

optional<T> stores T inline. An optional<std::array<int, 10000>> is a large stack object. For large types that are rarely present, prefer std::unique_ptr<T> (heap, nullable) over optional<T> (stack, inline).

Quick Reference

OperationSyntax
Hold a valuestd::optional<T> opt = value;
Hold nothingstd::optional<T> opt; or = std::nullopt
Check presenceif (opt) or opt.has_value()
Access (unsafe)*opt
Access (throws if empty)opt.value()
Access with defaultopt.value_or(default)
Construct in-placeopt.emplace(args...)
Clearopt.reset() or opt = std::nullopt
Chain (C++23).and_then(f) / .transform(f) / .or_else(f)
Header<optional>

C++23 Monadic Pipeline Composition

The monadic operations and_then, transform, and or_else turn scattered if chains into a single, readable pipeline. Each method receives a callable, applies it only when the optional is non-empty (or, for or_else, when it is empty), and returns a new optional. Because each call returns an optional, the results chain naturally without intermediate variables.

and_then is for callables that themselves return optional<U>. It unpacks the current value, passes it to the function, and returns whatever the function returns — preserving the "maybe nothing" contract all the way through:

cpp
#include <optional>
#include <string>
#include <charconv>

// Each step may independently fail
std::optional<int> parse_port(std::string_view s) {
    int result{};
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), result);
    if (ec != std::errc{} || result < 1 || result > 65535)
        return std::nullopt;
    return result;
}

std::optional<std::string> resolve_host(int port) {
    if (port == 443) return std::string{"example.com"};
    return std::nullopt;
}

// Pipeline: parse -> resolve -> uppercase
auto host = parse_port("443")
    .and_then(resolve_host)
    .transform([](std::string s) {
        for (char& c : s) c = static_cast<char>(std::toupper(c));
        return s;
    })
    .or_else([] { return std::optional<std::string>{"UNKNOWN"}; });

// host contains "EXAMPLE.COM"

transform differs from and_then in one key way: its callable returns a plain T, not an optional<T>. The method wraps the result for you. Use transform for infallible transformations and and_then for steps that can themselves fail.

optional with Reference Types

std::optional<T&> is not permitted by the standard — but the need for an optional reference arises frequently, and two idiomatic workarounds exist. The prohibition exists because optional references would create ambiguity around rebinding vs. assigning through, which the committee could not resolve cleanly before C++17 shipped.

The first workaround is a raw pointer. A nullable pointer to T is semantically identical to an optional reference: it either points at an existing object or is null. Prefer this when the pointer is non-owning and the pointed-to object has a longer lifetime than the pointer:

cpp
// Scan a list; return a pointer to the first match, or nullptr
const std::string* find_keyword(const std::vector<std::string>& words,
                                std::string_view target) {
    for (const auto& w : words)
        if (w == target) return &w;
    return nullptr;
}

// Caller checks before using
if (const auto* kw = find_keyword(words, "constexpr"))
    std::cout << "Found: " << *kw << '\n';

The second workaround is std::optional<std::reference_wrapper<T>>, which gives you the optional vocabulary (.has_value(), .value_or()) while holding a reference rather than a copy:

cpp
#include <optional>
#include <functional>

std::optional<std::reference_wrapper<int>>
find_first_even(std::vector<int>& v) {
    for (int& x : v)
        if (x % 2 == 0) return std::ref(x);
    return std::nullopt;
}

std::vector<int> nums = {1, 3, 4, 7};
if (auto ref = find_first_even(nums)) {
    ref->get() *= 10;   // modifies the element in-place
}
// nums is now {1, 3, 40, 7}

For most codebases, the raw pointer form is simpler and equally expressive. Reach for reference_wrapper only when you want the optional API or when you are storing the result in a container alongside regular optional<T> values.

optional vs. Nullable Pointer

The choice between std::optional<T> and T* comes down to ownership and the semantics of "absent": optional owns its value; a pointer borrows a value that lives elsewhere. Both model "maybe something, maybe nothing," but they answer different questions.

Use std::optional<T> when:

  • The function computes a value that may not exist (e.g., parsing, searching by key).
  • The caller should own the result — no allocation, no lifetime question.
  • The absent case means "not found" or "not applicable," not "an error occurred."

Use T* (or std::unique_ptr<T>) when:

  • The value's lifetime is managed elsewhere and you only need to refer to it.
  • The type is not copyable/movable (though optional supports non-movable types via emplace).
  • You need to distinguish between "absent," "present but null," and "present with a value" — three states that a single optional cannot represent.
cpp
// optional: function owns the result, no heap, clear intent
std::optional<Config> load_config(std::string_view path);

// pointer: caller does NOT own; the Config lives in a registry
const Config* lookup_config(std::string_view name);

// unique_ptr: heap-allocated, owned, nullable (useful for polymorphic types)
std::unique_ptr<Shape> create_shape(ShapeKind k);

A common rule of thumb: if you would write return T{...} on the happy path, prefer optional<T>. If you would write return &some_existing_thing, prefer a pointer.

Performance Characteristics

std::optional<T> stores its payload entirely within its own footprint — no heap allocation ever, no indirection, no reference counting. The layout is approximately one T object plus one bool flag, padded to satisfy alignment:

cpp
#include <optional>
#include <cstdio>

// Typical sizes (platform-dependent; these are x86-64 results)
static_assert(sizeof(std::optional<bool>)   ==  2);  // 1 + 1 flag
static_assert(sizeof(std::optional<int>)    ==  8);  // 4 + 1 flag + 3 padding
static_assert(sizeof(std::optional<double>) == 16);  // 8 + 1 flag + 7 padding
// optional<std::string> = sizeof(string) rounded up to alignment

Because the value is inline, accessing *opt is a single pointer dereference to the stack (or wherever the optional lives) — no cache miss from chasing a heap pointer. Copying an optional<T> is exactly as expensive as copying a T; the flag copy is negligible.

The size overhead matters when you hold large optionals in hot data structures. An optional<std::array<double, 512>> is 4 KB on the stack whether it is empty or full. For types where "rarely present" and "very large" are both true, a std::unique_ptr<T> (8 bytes when null, heap allocation only when non-null) may be more cache-friendly overall.

std::optional with Non-Movable Types

std::optional can hold non-movable, non-copyable types as long as you construct in place with emplace rather than assigning from a temporary. The optional stores the object directly in its internal buffer using placement new, so no copy or move is ever required.

cpp
#include <optional>
#include <mutex>

// std::mutex is neither copyable nor movable
std::optional<std::mutex> maybe_mutex;

// Direct construction into the optional's storage — no move needed
maybe_mutex.emplace();   // constructs std::mutex in place

// Access normally
maybe_mutex->lock();
maybe_mutex->unlock();

// Reset destroys in place and marks empty
maybe_mutex.reset();

The key constraint is that assignment from a value (maybe_mutex = std::mutex{}) still requires moveability, because it constructs a temporary first. Always use emplace(args...) when the held type is non-movable. Checking for presence and resetting work the same as for any other optional.

Pitfalls: UB vs. Exception on Empty Access

Dereferencing an empty optional with *opt or opt->member is undefined behaviour — it does not throw, it does not crash predictably, and sanitisers may not always catch it. The only safe ways to extract a value are through an explicit check, value(), or value_or().

cpp
std::optional<int> empty;

// UB — no check, no throw, just chaos
int x = *empty;          // undefined behaviour

// Safe — throws std::bad_optional_access if empty
int y = empty.value();   // throws here

// Safe — returns a default, never throws
int z = empty.value_or(-1);   // z == -1

// Idiomatic — check before dereference
if (empty) {
    int w = *empty;   // fine, we know it's non-empty
}

std::bad_optional_access derives from std::exception, so it propagates through normal exception-handling infrastructure. Use .value() in application code where empty is unexpected; use the if (opt) guard in performance-sensitive code where you must avoid the branch overhead of the throw path. Never use *opt without a prior check in the same scope.

Returning optional from Functions: Parse, Don't Validate

The "parse, don't validate" pattern uses optional return types to fuse the question "is this valid?" with the action "give me the result" — the caller receives either a ready-to-use value or nothing, with no redundant re-validation. When a function returns bool and fills an out-parameter, callers can call the function and then still use the out-parameter even if the function returned false. An optional makes that impossible.

cpp
#include <optional>
#include <string>
#include <string_view>

// Validates AND produces the result in one step.
// The caller cannot accidentally use an invalid port number.
std::optional<uint16_t> parse_port(std::string_view text) {
    unsigned long n{};
    try { n = std::stoul(std::string(text)); }
    catch (...) { return std::nullopt; }
    if (n == 0 || n > 65535) return std::nullopt;
    return static_cast<uint16_t>(n);
}

// Contrast with the validation-then-use anti-pattern:
bool is_valid_port(std::string_view text);    // forces a second parse
int  get_port(std::string_view text);         // what does it return if invalid?

// With parse-don't-validate:
if (auto port = parse_port(argv[1])) {
    bind_socket(*port);   // port is guaranteed valid here
} else {
    std::cerr << "Invalid port\n";
}

Apply this pattern whenever the "is it valid?" and "give me the value" questions have the same answer. The function does the work once; the optional encodes the outcome.

What's Next

  • std::expected — like optional, but carries an error value instead of just "nothing" — the C++23 approach to error handling without exceptions.
  • std::variant — holds exactly one of several types; optional<T> is effectively variant<T, std::monostate>.
  • Exception Handling — when to use exceptions vs. optional vs. expected.