std::optional: Representing Optional Values
C++17’s std::optional<T> represents a value that may or may not be present, avoiding special sentinel values or pointers.
Basic Usage
#include <optional>
std::optional<int> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // No value
}
}
auto result = parse_int("42");
if (result) { // or result.has_value()
std::cout << *result << '\\n'; // Dereference to get value
} else {
std::cout << "Parse failed\\n";
}
Accessing Values
std::optional<int> opt = 42;
// Safe access with default
int value = opt.value_or(0); // Returns 42, or 0 if empty
// Direct access (throws if empty)
int v1 = opt.value(); // Throws std::bad_optional_access if empty
// Unsafe access (UB if empty)
int v2 = *opt; // Like pointer dereference
// Check before access
if (opt.has_value()) {
std::cout << opt.value();
}
Creating Optionals
std::optional<int> empty; // No value
std::optional<int> zero{0}; // Contains 0
std::optional<int> forty_two = 42;
std::optional<int> none = std::nullopt;
// In-place construction
std::optional<std::string> str{std::in_place, 10, 'x'}; // "xxxxxxxxxx"
Modifying Optionals
std::optional<int> opt;
opt = 42; // Assign value
opt.reset(); // Clear value (now empty)
opt.emplace(100); // Construct value in-place
Monadic Operations (C++23)
std::optional<int> opt = 42;
// and_then: Apply function returning optional
auto result = opt.and_then([](int x) -> std::optional<int> {
return x > 0 ? std::optional{x * 2} : std::nullopt;
});
// transform: Apply function returning value
auto doubled = opt.transform([](int x) { return x * 2; });
// or_else: Provide alternative
auto val = std::optional<int>{}.or_else([]{ return std::optional{42}; });
Use Cases
class Config {
std::optional<std::string> username_;
std::optional<int> port_;
public:
void set_username(const std::string& name) { username_ = name; }
std::optional<std::string> get_username() const { return username_; }
int get_port() const { return port_.value_or(8080); } // Default port
};
std::variant: Type-Safe Unions
std::variant is a type-safe union that can hold one of several types at any time.
Basic Usage
#include <variant>
std::variant<int, double, std::string> value;
value = 42; // Holds int
value = 3.14; // Now holds double
value = "hello"; // Now holds string
// Access by index
int i = std::get<0>(value); // Throws if not int
// Access by type (must be unique)
std::string s = std::get<std::string>(value);
// Safe access
if (auto ptr = std::get_if<std::string>(&value)) {
std::cout << *ptr;
}
Visiting Variants
std::variant<int, double, std::string> v = 42;
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << '\\n';
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << arg << '\\n';
} else {
std::cout << "string: " << arg << '\\n';
}
}, v);
Overloaded Pattern
Helper for multiple lambdas:
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>; // Deduction guide
std::variant<int, double, std::string> v = "hello";
std::visit(overloaded{
[](int x) { std::cout << "int: " << x; },
[](double x) { std::cout << "double: " << x; },
[](const std::string& x) { std::cout << "string: " << x; }
}, v);
Variant State
std::variant<int, double> v = 3.14;
std::cout << v.index(); // 1 (double is second alternative)
if (std::holds_alternative<double>(v)) {
std::cout << "Holds double\\n";
}
Error Handling with Variant
Alternative to exceptions:
struct Error {
std::string message;
};
std::variant<int, Error> divide(int a, int b) {
if (b == 0) {
return Error{"Division by zero"};
}
return a / b;
}
auto result = divide(10, 2);
std::visit(overloaded{
[](int value) { std::cout << "Result: " << value; },
[](const Error& e) { std::cerr << "Error: " << e.message; }
}, result);
variant vs optional vs union
| Feature | optional | variant | union |
|---|---|---|---|
| Type Safety | Yes | Yes | No |
| Knows Type | Yes | Yes | No |
| Empty State | Yes | No* | N/A |
| Non-Trivial Types | Yes | Yes | No |
*variant has std::monostate for empty state
std::variant<std::monostate, int, double> v; // Default: monostate (empty)
Conceptual Check
What's the key advantage of std::optional over using nullptr or -1 as sentinel values?
Runtime Environment
Interactive Lab
1#include <iostream>
2#include <optional>
3#include <variant>
4#include <string>
5
6std::optional<int> safe_divide(int a, int b) {
7 if (b == 0) return std::nullopt;
8 return a / b;
9}
10
11int main() {
12 auto r1 = safe_divide(10, 2);
13 std::cout << "10/2 = " << r1.value_or(-1) << '\n';
14
15 auto r2 = safe_divide(10, 0);
16 std::cout << "10/0 = " << r2.value_or(-1) << '\n';
17
18 std::variant<int, double, std::string> v = 42;
19 std::cout << "Variant holds index " << v.index() << '\n';
20
21 v = "hello";
22 std::visit([](auto&& arg) {
23 std::cout << "Value: " << arg << '\n';
24 }, v);
25
26 return 0;
27}
System Console
Waiting for signal...