C++ std::optional Guide | nullopt, Monadic Ops (C++23), and Patterns

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 nullptr every time is inconvenient
  • Partial initialization — Some fields of the object are optional. Pointers are a memory management burden.

Problems with existing methods:

methodProblem
nullptrMemory management burden, risk of crash when dereferenced
special value (-1, INT_MIN)Difficult to distinguish from valid values, not type safe
exceptionExpected 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

  1. Problem scenario: Handling situations where there is no value
  2. std::optional basics
  3. C++23 monadic operations
  4. Practical error handling pattern
  5. Performance considerations
  6. Comparison with other types
  7. Complete example collection
  8. Frequently occurring mistakes and solutions
  9. Best practices/best practices
  10. Production Patterns
  11. 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;
}
Featuresoptionalvariant<T, E>
Usewith or without valueOne of several types
error informationnone (nullopt only)Error type can be saved
sizesizeof(T) + 1max(sizeof(T), sizeof(E)) + tag
usesimple failureDetailed 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;
}
Featuresoptionalexception
When to useExpected failureexceptional circumstances
PerformanceFast (quarterly)Slow (stack unwinding)
error informationNonedetailed message
Force processingNoYes (requires catch)
Code flowexplicitimplicit

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

SituationUse or notalternative
Search may fail✅ Use-
Missing setting value✅ Use-
Parse failed✅ Use-
always has value❌ Not necessaryGeneral type
Detailed error information required❌ Inappropriatevariant, expected
Performance Critical❌ Carefullyspecial values, pointers
Save reference❌ Impossiblepointer, 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

  1. 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?
  2. 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;
    }
  3. 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]");
  4. 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

SymptomsCauseSolution
bad_optional_accessNo value when calling value()Use value_or() or check has_value()
Compilation error: optional<T&>Reference type not possibleoptional<reference_wrapper> or use a pointer
poor performanceCopy large type by valuePass by const reference
nested optionaloptional<optional>Reexamine design, consider variants
chaining complextoo deep and_thenSeparate by intermediate variable

Optional vs other methods performance comparison

methodmemory overheadRunning speedSafetyDifficulty of use
optionalsizeof(T) + 1 to 8 bytesFastHighEasy
T* (pointer)8 bytesVery fastlowmiddle
unique_ptr8 bytes + heap allocationslowHighmiddle
exception0 (only on failure)very slowHighDifficulty
special value0Very fastlowEasy

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.


  • 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