Vocabulary Types

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

"Vocabulary types" are small, general-purpose types the whole standard library (and most C++ APIs) share a common understanding of — using them in your own interfaces means callers already know how to use them.

std::pair and std::tuple

#include <utility>
#include <tuple>
#include <iostream>

std::pair<int, std::string> makePair() { return {1, "one"}; }

std::tuple<int, std::string, double> makeTuple() { return {1, "one", 1.5}; }

int main() {
    auto [num, name] = makePair();              // structured bindings unpack a pair
    auto [n2, s2, d2] = makeTuple();              // and a tuple, of any size

    auto t = std::make_tuple(1, "one", 1.5);
    std::cout << std::get<0>(t) << ' ' << std::get<2>(t) << '\n';   // access by index
    (void)num; (void)name; (void)n2; (void)s2; (void)d2;
}

std::optional and Monadic Operations

std::optional<T> (C++17) represents "a T, or nothing" without a sentinel value or a separate boolean flag:

#include <optional>
#include <string>
#include <iostream>

std::optional<int> parseInt(const std::string& s) {
    try { return std::stoi(s); } catch (...) { return std::nullopt; }
}

int main() {
    auto result = parseInt("42");
    if (result) { std::cout << *result << '\n'; }        // check + dereference
    std::cout << result.value_or(-1) << '\n';               // default if empty

    auto chained = parseInt("42")
        .transform([](int x) { return x * 2; })              // C++23: map the contained value, if present
        .and_then([](int x) -> std::optional<int> {           // C++23: chain another optional-returning step
            return x > 0 ? std::optional(x) : std::nullopt;
        })
        .or_else([]() -> std::optional<int> { return 0; });    // C++23: fallback if empty at this point
    std::cout << chained.value_or(-1) << '\n';
}

The monadic operations (transform/and_then/or_else, C++23) chain optional-producing steps without a pyramid of nested if (opt) checks.

std::variant and std::visit

std::variant<Ts…​> is a type-safe union — it holds exactly one of its alternative types at a time, and accessing the wrong one throws std::bad_variant_access rather than reading garbage:

#include <variant>
#include <string>
#include <iostream>

using Value = std::variant<int, double, std::string>;

std::string describe(const Value& v) {
    return std::visit([](const auto& held) -> std::string {
        using T = std::decay_t<decltype(held)>;
        if constexpr (std::is_same_v<T, int>) return "int: " + std::to_string(held);
        else if constexpr (std::is_same_v<T, double>) return "double: " + std::to_string(held);
        else return "string: " + held;
    }, v);
}

int main() {
    Value v = 42;
    std::cout << describe(v) << '\n';
    v = "hello";
    std::cout << describe(v) << '\n';
    std::cout << std::holds_alternative<std::string>(v) << '\n';   // 1 (true)
}

std::visit with if constexpr on the visitor is the idiomatic way to dispatch over every alternative exhaustively, with a compile error if a new alternative type is added and forgotten.

std::any

std::any holds a value of any copyable type, checked only at run time — the type-erasure counterpart to `variant’s closed, compile-time-known set of alternatives:

#include <any>
#include <string>
#include <iostream>

int main() {
    std::any a = 42;
    a = std::string("hello");             // can hold a completely different type later
    if (auto* s = std::any_cast<std::string>(&a)) {
        std::cout << *s << '\n';
    }
    try {
        std::any_cast<int>(a);              // throws std::bad_any_cast: a currently holds a string, not an int
    } catch (const std::bad_any_cast& e) {
        std::cout << e.what() << '\n';
    }
}

Prefer std::variant whenever the set of possible types is known ahead of time — it is faster (no allocation, compile-time dispatch) and safer (exhaustiveness-checkable via visit).

std::expected

C++23’s std::expected<T, E> represents "a T, or an E explaining why not" — an alternative to exceptions for expected, recoverable failures, more explicit in a function’s signature than a thrown exception would be:

#include <expected>
#include <string>
#include <iostream>

std::expected<int, std::string> parsePositive(const std::string& s) {
    int value;
    try { value = std::stoi(s); } catch (...) { return std::unexpected("not a number"); }
    if (value <= 0) return std::unexpected("must be positive");
    return value;
}

int main() {
    auto result = parsePositive("42");
    if (result) { std::cout << *result << '\n'; }
    else { std::cout << "error: " << result.error() << '\n'; }

    auto bad = parsePositive("-5");
    std::cout << bad.value_or(0) << '\n';
}

See Error Handling for a fuller comparison of expected against exceptions and error_code.

<expected> compiles under this environment’s g++ 13 but not under clang++ 18 paired with the same libstdc++ — libstdc++'s header gates on __cpp_concepts >= 202002L, and Clang 18 still reports the earlier Concepts-TS value 201907L for that macro even in -std=c++23 mode. The syntax above is standard-conformant C++23 (verified against cppreference and the working draft) and compiles cleanly with g++ -std=c++23 -Wall -Wextra; a newer Clang, or Clang paired with libc++ instead of libstdc++, resolves this too.

std::reference_wrapper

Containers and std::optional/std::pair cannot directly hold a reference (references aren’t rebindable/assignable, which containers require); std::reference_wrapper<T> (usually spelled std::ref/ std::cref) wraps one in a copyable, assignable object that still behaves like a reference at the point of use:

#include <functional>
#include <vector>
#include <iostream>

int main() {
    int a = 1, b = 2, c = 3;
    std::vector<std::reference_wrapper<int>> refs = {std::ref(a), std::ref(b), std::ref(c)};
    for (int& r : refs) { r *= 10; }               // mutates a, b, c through the wrapper
    std::cout << a << ' ' << b << ' ' << c << '\n';  // 10 20 30
}