std::optional for Nullable Values
Prerequisites: Value semantics, move semantics, std::nullopt
Standard: C++17 (header <optional>)
HOW OPTIONAL STORES ITS VALUE:
std::optional<T> is laid out roughly as:
struct optional<T> {
bool has_value_; // engagement flag
aligned_storage_for<T> storage_; // raw bytes, sizeof(T)
};
- When EMPTY: has_value_ is false, storage_ is uninitialized raw bytes.
No constructor of T runs -- this is why optional<T> is cheap to
default-construct even if T is expensive.
- When ENGAGED: has_value_ is true, and a T object has been constructed
in-place inside storage_ (using placement new).
- sizeof(optional<T>) is approximately sizeof(T) + sizeof(bool), rounded
up for alignment. For example, optional<int> is typically 8 bytes
(4 for int + 1 for bool + 3 padding).
- NO HEAP ALLOCATION ever occurs -- the value lives entirely inside the
optional object itself, on the stack or wherever the optional resides.
- Assignment or emplace() uses placement new to construct T in the
internal buffer. If a T was already engaged, its destructor is called
first before constructing the new value.
- reset() or ~optional() calls T's destructor if engaged, then sets the
flag to false. If already empty, it is a no-op. Function that might not find a result
Watch out: accessing an empty optional via
operator* or operator-> is undefined behavior.
Always check has_value() or use value_or().
HOW OPTIONAL RETURN WORKS:
- `return static_cast<int>(i)` implicitly constructs an
optional<int> containing the value. This works because
optional<T> has a non-explicit constructor that accepts T
(or anything convertible to T), so the compiler inserts an
implicit conversion: return optional<int>(static_cast<int>(i));
- `return std::nullopt` constructs an empty optional. nullopt_t
is a special tag type whose sole purpose is to signal "no value".
optional<T> has a constructor that accepts nullopt_t.
- At the call site, the caller's `optional<int> idx` is constructed
directly from the return value. Thanks to copy elision (guaranteed
since C++17), no extra copy or move occurs -- the returned optional
is constructed directly in the caller's storage. std::optional<int> find_index(const std::vector<int>& vec, int target) {
for (size_t i = 0; i < vec.size(); ++i) {
if (vec[i] == target) {
return static_cast<int>(i); // Found!
}
}
return std::nullopt; // Not found
}
// Optional with complex type
struct User {
std::string name;
int age;
};
std::optional<User> find_user(const std::map<int, User>& db, int id) {
if (auto it = db.find(id); it != db.end()) {
return it->second;
}
return std::nullopt;
}
// Optional for lazy initialization
class Config {
mutable std::optional<std::string> cached_value_;
public:
const std::string& get_value() const {
if (!cached_value_) {
// Expensive computation, done only once
cached_value_ = "computed_value";
std::cout << "(computing...)\n";
}
return *cached_value_;
}
};int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
// Check if optional has value
if (auto idx = find_index(numbers, 30); idx.has_value()) {
std::cout << std::format("Found 30 at index {}\n", *idx);
}
// value_or() for default
auto idx = find_index(numbers, 99);
std::cout << std::format("Index of 99: {}\n", idx.value_or(-1));
// Boolean context
if (auto found = find_index(numbers, 20)) {
std::cout << std::format("20 is at index {}\n", *found);
} else {
std::cout << "20 not found\n";
}
// With complex types
std::map<int, User> users = {
{1, {"Alice", 30}},
{2, {"Bob", 25}}
};
if (auto user = find_user(users, 1)) {
std::cout << std::format("Found: {} (age {})\n",
user->name, user->age);
}
if (auto user = find_user(users, 99)) {
std::cout << "Found user 99\n";
} else {
std::cout << "User 99 not found\n";
}
// Transforming optionals (C++23 has monadic ops, but we can do:)
//
// HOW C++23 MONADIC OPERATIONS WORK:
// C++23 adds three member functions that let you chain optional
// operations without nested if-checks:
//
// and_then(f): if engaged, calls f(value) which must return
// optional<U>. If empty, returns empty optional<U>.
// Use when f itself might fail (returns optional).
//
// transform(f): if engaged, calls f(value) and WRAPS the result
// in optional<U>. If empty, returns empty optional<U>.
// Use when f always succeeds (returns plain U).
//
// or_else(f): if EMPTY, calls f() which must return optional<T>.
// If engaged, returns *this unchanged.
// Use to provide a fallback computation.
//
// Example (C++23):
// find_index(numbers, 30)
// .transform([](int i) { return i * 2; }) // double the index
// .or_else([] { return std::optional<int>{0}; }); // default to 0
//
// The lambda below is the pre-C++23 equivalent of .transform():
auto double_if_found = [&](int target) -> std::optional<int> {
if (auto idx = find_index(numbers, target)) {
return *idx * 2;
}
return std::nullopt;
};
std::cout << std::format("Double index of 30: {}\n",
double_if_found(30).value_or(-1));
// Lazy initialization
Config config;
std::cout << "First call: " << config.get_value() << '\n';
std::cout << "Second call: " << config.get_value() << '\n';
// Creating optional in-place
std::optional<std::vector<int>> opt_vec;
opt_vec.emplace(5, 42); // Creates vector of 5 42s in-place
std::cout << std::format("Vector size: {}\n", opt_vec->size());
return 0;
}