C++ std::optional Guide | nullopt, Monadic Ops (C++23), and Patterns
이 글의 핵심
From optional basics to C++23 monadic operations—patterns for missing values without raw pointers or heavy exceptions.
Introduction: “It may not have value, so how do I express it?”
Problems encountered in practice
We often encounter situations like this during C++ development:
- Search Failed — Searched by user ID, but nothing was found. return
nullptr? exception? Special value -1? - Missing configuration value — Specific key is missing from configuration file. How to handle defaults?
- Parse Failed — Failed to convert string to number. Exceptions are excessive and error codes are inconvenient.
- Cache Miss — No data in cache. Checking
nullptrevery time is inconvenient - Partial initialization — Some fields of the object are optional. Pointers are a memory management burden.
Problems with existing methods:
| method | Problem |
|---|---|
nullptr | Memory management burden, risk of crash when dereferenced |
| special value (-1, INT_MIN) | Difficult to distinguish from valid values, not type safe |
| exception | Expected failures are overkill, performance overhead |
std::pair<bool, T> | Long-winded, prone to mistakes |
Resolved with std::optional:
// ❌ Conventional method
int* findUser(int id) {
// ...
return nullptr; // Memory management required
}
// ✅ Optional use
std::optional<int> findUser(int id) {
// ...
return std::nullopt; // safe and clear
}
target:
- std::optional basics (creation, access, check)
- C++23 monadic operations (and_then, or_else, transform)
- Practical patterns (error handling, API design, chaining)
- Performance considerations (when not to use)
- Comparison with other types (variant, expected, exception)
- Frequent mistakes and solutions
- Production Pattern
Required Environment: C++17 or higher (C++23 features listed separately)
index
- Problem scenario: Handling situations where there is no value
- std::optional basics
- C++23 monadic operations
- Practical error handling pattern
- Performance considerations
- Comparison with other types
- Complete example collection
- Frequently occurring mistakes and solutions
- Best practices/best practices
- Production Patterns
- Organization and checklist
1. Problem Scenario: Handling a Missing Value Situation
Scenario 1: Database lookup failure
// ❌ Use of pointers (memory management burden)
User* findUserById(int id) {
// DB query
if (/* found */) {
return new User{id, "Alice"}; // 💥 Who deletes?
}
return nullptr;
}
// use
User* user = findUserById(123);
if (user != nullptr) {
std::cout << user->name << std::endl;
delete user; // 💥 Memory leak if you forget
}
Caution: The pattern of passing DB search results to new is prone to leaks when exceptions or early returns occur. If ownership is required, consider unique_ptr first, and if it “may not exist”, consider optional first.
// ✅ Optional use (safe and clear)
std::optional<User> findUserById(int id) {
// DB query (actually execute DB query)
if (/* if the user was found */) {
// Returns a User object wrapped in optional
return User{id, "Alice"};
}
// If not found, return nullopt (specifying that there is no value)
return std::nullopt;
}
// Example usage
if (auto user = findUserById(123)) {
// If user has a value (found), execute this block
std::cout << user->name << std::endl; // ✅ Automatic cleanup, no memory management required
}
// When the user leaves the if block, the user is automatically destroyed.
Scenario 2: Parsing configuration file
// ❌ Use of special values (difficult to distinguish from valid values)
int getTimeout(const Config& config) {
if (config.has("timeout")) {
return config.getInt("timeout");
}
return -1; // 💥 -1 may be a valid value
}
// use
int timeout = getTimeout(config);
if (timeout == -1) { // 💥 It is unclear whether -1 is the actual value or an error.
timeout = 30; // default
}
// ✅ Use optional (clear meaning)
std::optional<int> getTimeout(const Config& config) {
if (config.has("timeout")) {
return config.getInt("timeout");
}
return std::nullopt;
}
// use
int timeout = getTimeout(config).value_or(30); // ✅ Concise and clear
Scenario 3: String parsing
// ❌ Use exceptions (overkill for expected failures)
int parseInt(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
throw std::runtime_error("Parse failed"); // 💥 Exceptions are costly
}
}
// ✅ Use optional (expected failure)
std::optional<int> parseInt(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
return std::nullopt; // ✅ Expected failures are optional.
}
}
// use
if (auto num = parseInt("123")) {
std::cout << "Parsed: " << *num << std::endl;
} else {
std::cout << "Parse failed" << std::endl;
}
Note: Empty strings, overflows, etc. are still subject to stoi’s exception policy. For super-fast routes, consider changing to from_chars, etc.
Scenario 4: Cache Lookup
// ❌ Use pair<bool, T> (wordy)
std::pair<bool, std::string> getCached(const std::string& key) {
if (cache.contains(key)) {
return {true, cache[key]};
}
return {false, ""}; // 💥 An empty string can also be a valid value
}
// use
auto [found, value] = getCached("user:123");
if (found) { // 💥 Easy to make mistakes
std::cout << value << std::endl;
}
// ✅ Optional use (simple and safe)
std::optional<std::string> getCached(const std::string& key) {
if (cache.contains(key)) {
return cache[key];
}
return std::nullopt;
}
// use
if (auto value = getCached("user:123")) {
std::cout << *value << std::endl; // ✅ Clear
}
flowchart TB
subgraph Problems["Situation with no value"]
P1[Search Failed]
P2[Setting Missing]
P3[parse failed]
P4[cache miss]
end
subgraph OldSolutions["Old Solutions (Problems)"]
O1[nullptr - memory management]
O2[Special value - ambiguous]
O3[Exception - Excessive]
O4[pair - wordy]
end
subgraph NewSolution["std optional"]
N1[Safety]
N2[Clear]
N3[Concise]
end
P1 --> O1
P2 --> O2
P3 --> O3
P4 --> O4
O1 --> N1
O2 --> N2
O3 --> N3
O4 --> N1
2. std::optional basis
Creation and initialization
#include <optional>
#include <iostream>
#include <string>
// 1. Create default (no value)
std::optional<int> opt1;
std::optional<int> opt2 = std::nullopt;
// 2. Initialize with value
std::optional<int> opt3 = 42;
std::optional<int> opt4{42};
// 3. make_optional
auto opt5 = std::make_optional<int>(42);
auto opt6 = std::make_optional<std::string>("Hello");
// 4. Create in_place (complex type)
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
std::optional<Point> opt7{std::in_place, 10, 20}; // Create Point(10, 20) directly
// 5. emplace (assign value later)
std::optional<Point> opt8;
opt8.emplace(30, 40); // Create Point(30, 40)
Checking and accessing values
flowchart TD
Start[Check optional value] --> HasValue{has_value?}
HasValue -->|true| Access[value access]
HasValue -->|false| Handle[handle nullopt]
Access --> Method1[value - exception possible]
Access --> Method2[operator* - UB available]
Access --> Method3[value_or - safe]
Method3 --> Safe [return default]
style Method3 fill:#90EE90
style Safe fill:#90EE90
#include <optional>
#include <iostream>
int main() {
std::optional<int> opt = 42;
// 1. has_value() - explicit check
if (opt.has_value()) {
std::cout << "Has value: " << opt.value() << std::endl;
}
// 2. operator bool - implicit conversion
if (opt) {
std::cout << "Has value: " << *opt << std::endl;
}
// 3. value() - exceptions may occur
try {
std::optional<int> empty;
int x = empty.value(); // 💥 std::bad_optional_access exception
} catch (const std::bad_optional_access& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
// 4. operator* - dereference (UB if no value)
std::optional<int> opt2 = 100;
std::cout << *opt2 << std::endl; // 100
// 5. operator-> - member access
std::optional<std::string> opt3 = "Hello";
std::cout << opt3->length() << std::endl; // 5
// 6. value_or() - Provides default value (safest)
std::optional<int> empty;
int x = empty.value_or(0); // 0 (default)
std::cout << x << std::endl;
return 0;
}
Note: When there is no value, operator* has undefined behavior and value() throws bad_optional_access. In defensive code, leave value_or or the preceding if as the default.
Editing and removing values
#include <optional>
#include <iostream>
int main() {
std::optional<int> opt = 42;
// 1. Change value by substitution
opt = 100;
std::cout << *opt << std::endl; // 100
// 2. reset() - remove value
opt.reset();
std::cout << opt.has_value() << std::endl; // false
// 3. nullopt assignment
opt = 42;
opt = std::nullopt;
std::cout << opt.has_value() << std::endl; // false
// 4. emplace() – Create a new value
opt.emplace(200);
std::cout << *opt << std::endl; // 200
// 5. swap()
std::optional<int> opt1 = 10;
std::optional<int> opt2 = 20;
opt1.swap(opt2);
std::cout << *opt1 << " " << *opt2 << std::endl; // 20 10
return 0;
}
Comparison operations
#include <optional>
#include <iostream>
int main() {
std::optional<int> a = 10;
std::optional<int> b = 20;
std::optional<int> empty;
// Compare optionals
std::cout << (a == b) << std::endl; // false
std::cout << (a < b) << std::endl; // true
std::cout << (a == empty) << std::endl; // false
// Direct comparison with value
std::cout << (a == 10) << std::endl; // true
std::cout << (a != 20) << std::endl; // true
// Compare with nullopt
std::cout << (empty == std::nullopt) << std::endl; // true
std::cout << (a != std::nullopt) << std::endl; // true
// Comparison Rules
// - nullopt < any value
// - If there are values, compare them.
std::optional<int> opt1 = 5;
std::optional<int> opt2;
std::cout << (opt2 < opt1) << std::endl; // true (nullopt < 5)
return 0;
}
3. C++23 monadic operations
and_then: Chaining (flatMap)
Available starting from C++23
#include <optional>
#include <iostream>
#include <string>
// User Lookup
std::optional<int> findUserId(const std::string& username) {
if (username == "alice") return 1;
if (username == "bob") return 2;
return std::nullopt;
}
// User email inquiry
std::optional<std::string> findEmail(int userId) {
if (userId == 1) return "[email protected]";
if (userId == 2) return "[email protected]";
return std::nullopt;
}
int main() {
// ❌ C++17 method (nested if)
auto userId = findUserId("alice");
if (userId) {
auto email = findEmail(*userId);
if (email) {
std::cout << "Email: " << *email << std::endl;
}
}
// ✅ C++23 and_then (Chaining)
auto email = findUserId("alice")
.and_then(findEmail);
if (email) {
std::cout << "Email: " << *email << std::endl;
}
// Handle failure cases naturally
auto noEmail = findUserId("charlie") // return nullopt
.and_then(findEmail); // not running
std::cout << noEmail.has_value() << std::endl; // false
return 0;
}
transform: Transform value (map)
#include <optional>
#include <iostream>
#include <string>
int main() {
std::optional<int> opt = 42;
// ❌ C++17 method
std::optional<std::string> str1;
if (opt) {
str1 = std::to_string(*opt);
}
// ✅ C++23 transform
auto str2 = opt.transform( {
return std::to_string(x);
});
std::cout << *str2 << std::endl; // "42"
// chaining
auto result = std::optional<int>{10}
.transform( { return x * 2; }) // 20
.transform( { return x + 5; }) // 25
.transform( { return std::to_string(x); }); // "25"
std::cout << *result << std::endl; // "25"
// Empty optional is not converted
std::optional<int> empty;
auto result2 = empty.transform( {
std::cout << "Not executed" << std::endl;
return x * 2;
});
std::cout << result2.has_value() << std::endl; // false
return 0;
}
or_else: Provides alternative value
#include <optional>
#include <iostream>
std::optional<int> getFromCache(const std::string& key) {
// cache lookup
return std::nullopt; // Miss Cathy
}
std::optional<int> getFromDatabase(const std::string& key) {
// DB query
return 42;
}
int main() {
// ❌ C++17 method
auto value1 = getFromCache("user:123");
if (!value1) {
value1 = getFromDatabase("user:123");
}
// ✅ C++23 or_else
auto value2 = getFromCache("user:123")
.or_else( { return getFromDatabase("user:123"); });
std::cout << *value2 << std::endl; // 42
// Multiple alternative source chaining
auto value3 = getFromCache("user:123")
.or_else( { return getFromDatabase("user:123"); })
.or_else( { return std::optional<int>{0}; }); // final default
return 0;
}
Practical Combinations: Complex Pipelines
#include <optional>
#include <iostream>
#include <string>
#include <map>
struct User {
int id;
std::string name;
std::optional<std::string> email;
};
std::map<int, User> users = {
{1, {1, "Alice", "[email protected]"}},
{2, {2, "Bob", std::nullopt}},
};
std::optional<User> findUser(int id) {
auto it = users.find(id);
if (it != users.end()) {
return it->second;
}
return std::nullopt;
}
int main() {
// User inquiry → Email extraction → Domain extraction
auto domain = findUser(1)
.and_then( { return u.email; })
.transform( {
size_t pos = email.find('@');
return email.substr(pos + 1);
});
if (domain) {
std::cout << "Domain: " << *domain << std::endl; // "example.com"
}
// User without email
auto noDomain = findUser(2)
.and_then( { return u.email; }) // return nullopt
.transform( {
// Not running
return email.substr(email.find('@') + 1);
});
std::cout << noDomain.has_value() << std::endl; // false
return 0;
}
4. Practical error handling patterns
Pattern 1: Parsing configuration file
#include <optional>
#include <iostream>
#include <map>
#include <string>
class Config {
private:
std::map<std::string, std::string> data_;
public:
void set(const std::string& key, const std::string& value) {
data_[key] = value;
}
std::optional<std::string> getString(const std::string& key) const {
auto it = data_.find(key);
if (it != data_.end()) {
return it->second;
}
return std::nullopt;
}
std::optional<int> getInt(const std::string& key) const {
return getString(key).and_then( -> std::optional<int> {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
});
}
std::optional<bool> getBool(const std::string& key) const {
return getString(key).transform( {
return s == "true" || s == "1";
});
}
};
int main() {
Config config;
config.set("port", "8080");
config.set("host", "localhost");
config.set("debug", "true");
config.set("invalid", "abc");
// If value exists, use; if not, default value.
int port = config.getInt("port").value_or(3000);
std::string host = config.getString("host").value_or("0.0.0.0");
bool debug = config.getBool("debug").value_or(false);
std::cout << "Port: " << port << std::endl;
std::cout << "Host: " << host << std::endl;
std::cout << "Debug: " << debug << std::endl;
// Handling invalid values
auto invalid = config.getInt("invalid");
if (!invalid) {
std::cerr << "Invalid integer value" << std::endl;
}
return 0;
}
Pattern 2: Cache System
#include <optional>
#include <map>
#include <string>
#include <chrono>
#include <iostream>
template<typename K, typename V>
class TimedCache {
private:
struct Entry {
V value;
std::chrono::steady_clock::time_point expires_at;
};
std::map<K, Entry> data_;
std::chrono::seconds default_ttl_;
public:
explicit TimedCache(std::chrono::seconds ttl = std::chrono::seconds{60})
: default_ttl_(ttl) {}
void put(const K& key, const V& value) {
auto expires = std::chrono::steady_clock::now() + default_ttl_;
data_[key] = {value, expires};
}
std::optional<V> get(const K& key) {
auto it = data_.find(key);
if (it == data_.end()) {
return std::nullopt; // Miss Cathy
}
// Check expiration
if (std::chrono::steady_clock::now() > it->second.expires_at) {
data_.erase(it);
return std::nullopt; // expired
}
return it->second.value;
}
// Using C++23 or_else
template<typename F>
V getOrCompute(const K& key, F&& compute) {
return get(key).or_else([&]() -> std::optional<V> {
V value = compute();
put(key, value);
return value;
}).value();
}
};
int main() {
TimedCache<std::string, int> cache{std::chrono::seconds{5}};
// save cache
cache.put("user:123", 42);
// cache lookup
if (auto value = cache.get("user:123")) {
std::cout << "Cached: " << *value << std::endl;
}
// If not, calculate and save
int score = cache.getOrCompute("user:456", {
std::cout << "Computing..." << std::endl;
return 100; // Assuming you searched in DB
});
std::cout << "Score: " << score << std::endl;
return 0;
}
Pattern 3: API response processing
#include <optional>
#include <string>
#include <iostream>
struct ApiResponse {
int status_code;
std::optional<std::string> body;
std::optional<std::string> error;
};
ApiResponse fetchData(const std::string& url) {
// HTTP request simulation
if (url == "https://api.example.com/users") {
return {200, R"({"users": []})", std::nullopt};
} else {
return {404, std::nullopt, "Not Found"};
}
}
int main() {
auto response = fetchData("https://api.example.com/users");
if (response.status_code == 200 && response.body) {
std::cout << "Success: " << *response.body << std::endl;
} else if (response.error) {
std::cerr << "Error: " << *response.error << std::endl;
}
// Utilizing C++23 transform
auto bodyLength = response.body.transform( {
return s.length();
});
std::cout << "Body length: " << bodyLength.value_or(0) << std::endl;
return 0;
}
5. Performance considerations
Memory overhead
#include <optional>
#include <iostream>
struct SmallType {
int x;
};
struct LargeType {
int data[1000];
};
int main() {
std::cout << "int: " << sizeof(int) << " bytes" << std::endl;
std::cout << "optional<int>: " << sizeof(std::optional<int>) << " bytes" << std::endl;
// Output: int: 4 bytes, optional<int>: 8 bytes (bool flag + padding)
std::cout << "SmallType: " << sizeof(SmallType) << " bytes" << std::endl;
std::cout << "optional<SmallType>: " << sizeof(std::optional<SmallType>) << " bytes" << std::endl;
std::cout << "LargeType: " << sizeof(LargeType) << " bytes" << std::endl;
std::cout << "optional<LargeType>: " << sizeof(std::optional<LargeType>) << " bytes" << std::endl;
// Even for large types, only the bool flag is added (approximately 1 byte + padding)
return 0;
}
Conclusion: The overhead of optional is mostly 1 byte + alignment padding.
When to avoid optional
// ❌ Bad use: always has value
std::optional<int> getUserAge(int userId) {
// Optional is not necessary if the age is always returned.
return 25;
}
// ✅ Good use: When it may not have value.
std::optional<int> getUserAge(int userId) {
if (userId < 0) {
return std::nullopt; // invalid user
}
return 25;
}
// ❌ Bad use: Performance critical loops
for (int i = 0; i < 1000000; ++i) {
std::optional<int> result = compute(i); // Create optional every time
if (result) {
process(*result);
}
}
// ✅ Good use: handling special values
for (int i = 0; i < 1000000; ++i) {
int result = compute(i); // -1 = failure
if (result != -1) {
process(result);
}
}
// ❌ Bad use: making references optional
// std::optional<T&> is not possible
// std::optional<std::reference_wrapper<T>> is complicated
// ✅ Good use: Using pointers
T* ptr = findObject(); // nullptr possible
Performance Benchmarks
#include <optional>
#include <chrono>
#include <iostream>
// 1. optional vs pointer
void benchmarkOptional() {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000000; ++i) {
std::optional<int> opt = (i % 2 == 0) ? std::optional<int>{i} : std::nullopt;
if (opt) {
volatile int x = *opt; // Avoid optimization
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "optional: " << duration.count() << "ms" << std::endl;
}
void benchmarkPointer() {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000000; ++i) {
int* ptr = (i % 2 == 0) ? new int(i) : nullptr;
if (ptr) {
volatile int x = *ptr;
delete ptr;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "pointer: " << duration.count() << "ms" << std::endl;
}
int main() {
benchmarkOptional(); // About 50ms
benchmarkPointer(); // Approximately 500ms (new/delete overhead)
// Optional is much faster!
return 0;
}
6. Compare with other types
optional vs variant
#include <optional>
#include <variant>
#include <string>
#include <iostream>
// optional: with or without value
std::optional<int> divide(int a, int b) {
if (b == 0) return std::nullopt;
return a / b;
}
// variant: one of several types
std::variant<int, std::string> parseValue(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
return "Parse error: " + str;
}
}
int main() {
// Use optional
if (auto result = divide(10, 2)) {
std::cout << "Result: " << *result << std::endl;
}
// Use variant
auto value = parseValue("123");
if (std::holds_alternative<int>(value)) {
std::cout << "Integer: " << std::get<int>(value) << std::endl;
} else {
std::cout << "Error: " << std::get<std::string>(value) << std::endl;
}
return 0;
}
| Features | optional | variant<T, E> |
|---|---|---|
| Use | with or without value | One of several types |
| error information | none (nullopt only) | Error type can be saved |
| size | sizeof(T) + 1 | max(sizeof(T), sizeof(E)) + tag |
| use | simple failure | Detailed error information required |
optional vs exception
#include <optional>
#include <stdexcept>
#include <iostream>
// use exceptions
int parseIntException(const std::string& str) {
try {
return std::stoi(str);
} catch (const std::exception& e) {
throw std::runtime_error("Parse failed: " + str);
}
}
// Use optional
std::optional<int> parseIntOptional(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
return std::nullopt;
}
}
int main() {
// Exception: Exceptional Circumstances
try {
int x = parseIntException("abc");
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
// optional: expected failure
if (auto x = parseIntOptional("abc")) {
std::cout << "Parsed: " << *x << std::endl;
} else {
std::cout << "Parse failed (expected)" << std::endl;
}
return 0;
}
| Features | optional | exception |
|---|---|---|
| When to use | Expected failure | exceptional circumstances |
| Performance | Fast (quarterly) | Slow (stack unwinding) |
| error information | None | detailed message |
| Force processing | No | Yes (requires catch) |
| Code flow | explicit | implicit |
optional vs expected (C++23 proposal)
// expected<T, E>: optional + error information
// To be standardized in C++23
template<typename T, typename E>
class expected {
// Save T or E
// Similar to optional<T> but includes error information
};
// Example usage
expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return unexpected{"Division by zero"};
}
return a / b;
}
// Compare to optional
std::optional<int> divideOptional(int a, int b) {
if (b == 0) return std::nullopt; // No error information
return a / b;
}
Related article: Learn practical patterns for using the two types together at Using Optional and Variant.
7. Complete set of examples
Example 1: JSON parser
#include <optional>
#include <string>
#include <map>
#include <iostream>
class JsonValue {
public:
std::map<std::string, std::string> data;
std::optional<std::string> getString(const std::string& key) const {
auto it = data.find(key);
return (it != data.end()) ? std::optional{it->second} : std::nullopt;
}
std::optional<int> getInt(const std::string& key) const {
return getString(key).and_then( -> std::optional<int> {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
});
}
std::optional<bool> getBool(const std::string& key) const {
return getString(key).transform( {
return s == "true";
});
}
};
int main() {
JsonValue json;
json.data["name"] = "Alice";
json.data["age"] = "30";
json.data["active"] = "true";
// safe access
auto name = json.getString("name").value_or("Unknown");
auto age = json.getInt("age").value_or(0);
auto active = json.getBool("active").value_or(false);
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "Active: " << active << std::endl;
return 0;
}
8. Common mistakes and solutions
Mistake 1: Exception when calling value()
// ❌ Wrong way
std::optional<int> empty;
int x = empty.value(); // 💥 std::bad_optional_access exception
// ✅ Correct method 1: check has_value()
if (empty.has_value()) {
int x = empty.value();
}
// ✅ Correct method 2: operator bool
if (empty) {
int x = *empty;
}
// ✅ Correct method 3: value_or() (safest)
int x = empty.value_or(0);
Mistake 2: Using it like a pointer
// ❌ Wrong way
std::optional<int> opt = 42;
if (opt != nullptr) { // 💥 Compilation error
}
// ✅ The right way
if (opt.has_value()) {
// ...
}
// or
if (opt) {
// ...
}
// Compare with nullopt
if (opt != std::nullopt) {
// ...
}
Mistake 3: Trying optional<T&>
// ❌ Wrong method (compilation error)
int x = 10;
// std::optional<int&> opt = x; // 💥 Impossible
// ✅ Correct method 1: reference_wrapper
std::optional<std::reference_wrapper<int>> opt = std::ref(x);
if (opt) {
opt->get() = 20; // x changed to 20
}
// ✅ Correct way 2: Use pointers (simpler)
std::optional<int*> opt2 = &x;
if (opt2) {
**opt2 = 30;
}
Mistake 4: Unnecessary copying
// ❌ Inefficient
std::optional<std::string> getLargeString() {
std::string large(10000, 'x');
return large; // Copy occurs (RVO may not work)
}
// ✅ Efficient (utilizing RVO)
std::optional<std::string> getLargeString() {
if (/* condition */) {
return std::string(10000, 'x'); // RVO
}
return std::nullopt;
}
// ✅ Movement explicit
std::optional<std::string> getLargeString() {
std::string large(10000, 'x');
return std::move(large); // movement
}
Mistake 5: Nesting optional<optional>
// ❌ Complex and confusing
std::optional<std::optional<int>> nested() {
return std::optional<int>{42}; // 💥 nested optional
}
// ✅ Single optional use
std::optional<int> simple() {
return 42;
}
// If you really need overlap, consider variant
std::variant<int, std::string, std::monostate> alternative() {
return 42;
}
Mistake 6: Overuse in performance-critical code
// ❌ Creating optional in hot loop
for (int i = 0; i < 1000000; ++i) {
std::optional<int> result = compute(i);
if (result) {
process(*result);
}
}
// ✅ Use special values (faster)
for (int i = 0; i < 1000000; ++i) {
int result = compute(i); // -1 = failure
if (result != -1) {
process(result);
}
}
9. Best practices/best practices
1. Use value_or() as default
// ✅ Concise and safe
int port = config.getInt("port").value_or(8080);
// ❌ Wordy
int port;
if (auto p = config.getInt("port")) {
port = *p;
} else {
port = 8080;
}
2. Used as function return type
// ✅ Express the possibility of failure as a type
std::optional<User> findUser(int id);
// ❌ Pointers (memory management burden)
User* findUser(int id);
// ❌ Exceptions (overkill for expected failures)
User findUser(int id); // Exception if not
3. Utilizing C++23 monadic operations
// ✅ Concise with chaining
auto email = findUser(123)
.and_then( { return u.getEmail(); })
.value_or("[email protected]");
// ❌ Nested if
std::string email = "[email protected]";
if (auto user = findUser(123)) {
if (auto e = user->getEmail()) {
email = *e;
}
}
4. Utilizing structured binding (C++17)
struct Result {
std::optional<int> value;
std::optional<std::string> error;
};
Result compute() {
// ...
return {42, std::nullopt};
}
// ✅ Structured bindings
auto [value, error] = compute();
if (value) {
std::cout << "Success: " << *value << std::endl;
} else if (error) {
std::cerr << "Error: " << *error << std::endl;
}
5. Clear naming
// ✅ Clear function names
std::optional<User> tryFindUser(int id);
std::optional<int> maybeParseInt(const std::string& str);
// ❌ Ambiguous name
User getUser(int id); // What happens if you don't have it?
int parseInt(const std::string& str); // What if I fail?
10. production pattern
Pattern 1: Optional Chaining Helper
template<typename T, typename F>
auto map_optional(const std::optional<T>& opt, F&& func)
-> std::optional<std::invoke_result_t<F, T>> {
if (opt) {
return func(*opt);
}
return std::nullopt;
}
// use
std::optional<int> opt = 42;
auto result = map_optional(opt, { return x * 2; });
Pattern 2: Combining multiple optionals
template<typename T>
std::optional<std::vector<T>> collect_optionals(
const std::vector<std::optional<T>>& opts) {
std::vector<T> result;
for (const auto& opt : opts) {
if (!opt) {
return std::nullopt; // Failure if one is missing
}
result.push_back(*opt);
}
return result;
}
// use
std::vector<std::optional<int>> opts = {1, 2, 3};
if (auto values = collect_optionals(opts)) {
// All values are present
}
Pattern 3: Lazy evaluation
template<typename F>
class LazyOptional {
private:
F compute_;
mutable std::optional<std::invoke_result_t<F>> cache_;
public:
explicit LazyOptional(F func) : compute_(std::move(func)) {}
auto get() const -> std::optional<std::invoke_result_t<F>> {
if (!cache_) {
cache_ = compute_();
}
return cache_;
}
};
// use
LazyOptional expensive{ -> std::optional<int> {
// expensive calculation
return 42;
}};
// Compute only when needed
if (auto value = expensive.get()) {
std::cout << *value << std::endl;
}
11. Organize and Checklist
optional usage guide
| Situation | Use or not | alternative |
|---|---|---|
| Search may fail | ✅ Use | - |
| Missing setting value | ✅ Use | - |
| Parse failed | ✅ Use | - |
| always has value | ❌ Not necessary | General type |
| Detailed error information required | ❌ Inappropriate | variant, expected |
| Performance Critical | ❌ Carefully | special values, pointers |
| Save reference | ❌ Impossible | pointer, reference_wrapper |
Checklist
# ✅ When using optional
- [ ] Is this a situation where there may be no value?
- [ ] Is a default value provided with value_or()?
- [ ] Did you check has_value() before calling value()?
- [ ] Did you avoid unnecessary copying?
- [ ] Isn’t this performance-critical code?
- [ ] Are large types passed as const references?
# ✅ When designing API
- [ ] Does the function name imply an optional return? (try~, maybe~, find~)
- [ ] Is the nullopt return condition specified in the document?
- [ ] Have you considered alternatives (exception, variant, expected)?
- [ ] Is the chaining too deep? (Level 3 or lower recommended)
# ✅ When reviewing code
- Is it appropriate to use [ ] optional<bool>? (Is 3-state necessary?)
- [ ] optional<optional<T>> Is there any overlap?
- [ ] If error information is needed, have you considered the variant?
Practical tips: optional Use effectively
-
Express intent with function name
// ✅ Good name (optional return hint) std::optional<User> tryFindUser(int id); std::optional<int> maybeParseInt(const std::string& s); std::optional<Config> loadConfig(const std::string& path); // ❌ Ambiguous name User getUser(int id); // What if there isn't one? int parseInt(const std::string& s); // What if I fail? -
Based on value_or()
// ✅ Concise and safe int port = config.getInt("port").value_or(8080); // ❌ Wordy int port; auto opt = config.getInt("port"); if (opt.has_value()) { port = opt.value(); } else { port = 8080; } -
Using C++23 monadic operations
// ✅ Concise with chaining auto result = findUser(id) .and_then( { return u.getEmail(); }) .transform( { return e.toLowerCase(); }) .value_or("[email protected]"); -
Use with error logging
auto user = findUser(id); if (!user) { LOG_WARNING("User not found: " + std::to_string(id)); return std::nullopt; } return user->process();
Quick reference: optional Use decision tree
Can there be no value?
├─ Yes → Consider using optional
│ ├─ Error information required? → variant<T, Error> or expected<T, E>
│ ├─ Performance critical? → Consider special values or pointers
│ └─ General case → std::optional ✅
└─ No → Use general type
Troubleshooting: Quick problem solving
| Symptoms | Cause | Solution |
|---|---|---|
| bad_optional_access | No value when calling value() | Use value_or() or check has_value() |
| Compilation error: optional<T&> | Reference type not possible | optional<reference_wrapper |
| poor performance | Copy large type by value | Pass by const reference |
| nested optional | optional<optional | Reexamine design, consider variants |
| chaining complex | too deep and_then | Separate by intermediate variable |
Optional vs other methods performance comparison
| method | memory overhead | Running speed | Safety | Difficulty of use |
|---|---|---|---|---|
| optional | sizeof(T) + 1 to 8 bytes | Fast | High | Easy |
| T* (pointer) | 8 bytes | Very fast | low | middle |
| unique_ptr | 8 bytes + heap allocation | slow | High | middle |
| exception | 0 (only on failure) | very slow | High | Difficulty |
| special value | 0 | Very fast | low | Easy |
Next steps
- Learn how to represent one of several types in std::variant guide
- Learn how to use the two types in practice at Use of Optional and Variant
- Learn the basic concepts of exception handling at Exception Handling Basics
- Learn the selection criteria for exceptions and optional in Exception Handling Guide
- Learn how to use variant instead of inheritance in Polymorphism and Variant
FAQ
Q1: When do you use optional?
A:
- Cases where there may be no value
- Error indication (simple case)
- Null pointer replacement
Q2: optional vs pointer?
A:
- optional: value semantics, safety
- Pointers: reference semantics, risks
Q3: optional vs exception?
A:
- optional: expected failure
- Exception: Exceptional circumstances
Q4: What is the performance overhead?
A: Add one bool flag. You can almost ignore it.
Q5: What is optional<T&>?
A: Impossible. Use optional<reference_wrapper
Q6: What are optional learning resources?
A:
- cppreference.com
- “C++17: The Complete Guide”
- “Effective Modern C++“
Q7: When receiving optional as a function argument, do I need to use a const reference?
A: Pass small types by value and large types by const reference.
// Small types (int, double, etc.): by value
void process(std::optional<int> opt) {
if (opt) { /* ... */ }
}
// Large types (string, vector, etc.): as const references
void process(const std::optional<std::string>& opt) {
if (opt) { /* ... */ }
}
Q8: What are the precautions when using optional as a member variable?
A: Initialize explicitly in the constructor, and use safe access patterns.
class Config {
private:
std::optional<std::string> api_key_;
public:
Config() : api_key_(std::nullopt) {}
std::string getApiKey() const {
return api_key_.value_or("default_key");
}
};
Q9: When do you use optional?
A: Use when three states “true/false/don’t know” are required.
// User consent status
std::optional<bool> user_consent; // nullopt = don't ask yet
if (!user_consent) {
askForConsent();
} else if (*user_consent) {
proceed();
} else {
showError();
}
Q10: What if optional chaining becomes too deep?
A: Increase readability by separating them into intermediate variables.
// ✅ Separate by intermediate variables
auto user = getUser(id);
if (!user) return std::nullopt;
auto profile = user->getProfile();
if (!profile) return std::nullopt;
return profile->getEmail();
Good article to read together (internal link)
Here’s another article related to this topic.
- C++ variant | “Type-safe union” guide
- C++ std::optional·std::variant complete guide | Type safe instead of nullptr
- C++ exception handling | try/catch/throw “perfect cleanup” [error handling]
- C++ exception handling | try-catch-throw and exceptions vs. error codes, when and what to use
- C++ Modern Polymorphism Design: Composition Variant Instead of Inheritance
Keywords covered in this article (related search terms)
This article will be helpful if you search for C++, optional, C++17, C++23, nullable, optional value, monadic operation, error handling, etc.
Related articles
- C++ optional |
- C++ std::optional vs pointer |
- C++ Latest Features |
- C++ any |
- Modern C++ (C++11~C++20) Core Grammar Cheat Sheet | A glance at frequently used items in the workplace