std::optional and std::expected
2 min read
std::optional and std::expected
Not all errors are exceptional. When a function might legitimately fail to produce a value — parsing a number, looking up a key, opening a file — using exceptions for the failure case is heavyweight. std::optional and std::expected model this in the type system without throwing.
std::optional (C++17)
std::optional<T> holds either a T value or nothing (std::nullopt):
#include <optional>
#include <string>
#include <iostream>
std::optional<int> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
}
int main() {
auto a = parse_int("42");
auto b = parse_int("hello");
if (a) std::cout << "parsed: " << *a << "\n"; // parsed: 42
if (!b) std::cout << "parse failed\n"; // parse failed
// Safe access with value_or
std::cout << a.value_or(-1) << "\n"; // 42
std::cout << b.value_or(-1) << "\n"; // -1
// Throws std::bad_optional_access if empty
// std::cout << b.value() << "\n";
}
Chaining with optional
std::optional pairs naturally with early returns to keep code flat:
std::optional<std::string> read_env(const std::string& key) {
const char* val = std::getenv(key.c_str());
if (!val) return std::nullopt;
return std::string(val);
}
std::optional<int> get_port() {
auto s = read_env("PORT");
if (!s) return std::nullopt;
return parse_int(*s);
}
std::expected (C++23)
std::expected<T, E> carries either a success value of type T or an error value of type E. Unlike optional, it tells you why the operation failed:
#include <expected>
#include <string>
#include <system_error>
std::expected<int, std::string> parse_port(const std::string& s) {
try {
int n = std::stoi(s);
if (n < 1 || n > 65535)
return std::unexpected("port out of range: " + s);
return n;
} catch (...) {
return std::unexpected("not a number: " + s);
}
}
int main() {
auto r = parse_port("8080");
if (r) {
std::cout << "port: " << *r << "\n";
} else {
std::cout << "error: " << r.error() << "\n";
}
auto bad = parse_port("99999");
std::cout << bad.value_or(0) << "\n"; // 0 — fallback on error
}
std::expected with error codes
Using an enum for the error type gives structured, matchable errors:
enum class ParseError { EmptyInput, InvalidChar, Overflow };
std::expected<int, ParseError> parse_strict(const std::string& s) {
if (s.empty()) return std::unexpected(ParseError::EmptyInput);
for (char c : s) if (!std::isdigit(c)) return std::unexpected(ParseError::InvalidChar);
try { return std::stoi(s); }
catch (...) { return std::unexpected(ParseError::Overflow); }
}
When to use what
| Situation | Use |
|---|---|
| Value might be absent, no reason needed | std::optional<T> |
| Value might be absent, reason matters | std::expected<T, E> |
| Truly exceptional / unexpected failure | exceptions |
| Performance-critical hot path, known failure modes | error codes / return values |
Prefer optional and expected for ordinary failure modes — file not found, key not in map, invalid input. Reserve exceptions for truly unexpected situations: allocation failure, violated class invariants, I/O errors on required resources.
Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.
View book →