std::expected

Modern error handling without exceptions

🛡️ What is std::expected?

std::expected is a C++23 feature that represents a value that might contain either a successful result or an error. It provides a clean alternative to exception handling for error management.


#include <expected>
#include <iostream>

std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) return std::unexpected("Division by zero");
    return a / b;
}

int main() {
    auto result = divide(10, 2);
    if (result) std::cout << "Result: " << *result;
    else std::cout << "Error: " << result.error();
}
                                    

Output:

Result: 5

Key Features of std::expected

🚫

No Exceptions

Handle errors without throwing exceptions

// No try-catch needed

Explicit

Makes error handling visible in code

if (result.has_value())

Efficient

Zero-overhead error handling

// Fast error propagation
🔗

Chainable

Compose operations easily

result.and_then(func)

🔹 Basic Usage

Expected values simplify error handling by wrapping results in a type-safe container. Instead of using raw pointers or error codes, you create an Expected<T, E> object that holds either a valid value of type T or an error of type E. This approach makes function signatures clearer and enforces that callers explicitly handle both success and failure cases, reducing runtime crashes and undefined behavior in applications.

#include <expected>
#include <iostream>
#include <string>

std::expected<double, std::string> sqrt_safe(double x) {
    if (x < 0) {
        return std::unexpected("Cannot take square root of negative number");
    }
    return std::sqrt(x);
}

int main() {
    auto result1 = sqrt_safe(16.0);
    auto result2 = sqrt_safe(-4.0);
    
    if (result1) {
        std::cout << "sqrt(16) = " << *result1 << std::endl;
    }
    
    if (!result2) {
        std::cout << "Error: " << result2.error() << std::endl;
    }
    
    return 0;
}

Output:

sqrt(16) = 4
Error: Cannot take square root of negative number

🔹 Error Handling Patterns

Different strategies exist for managing expected values depending on context. You can use .value() to directly access the result—throwing an exception if an error is stored. Alternatively, .error() retrieves the error object for inspection. For safer access, methods like .has_value() check validity before proceeding. Pattern matching or visitor patterns can also be employed to handle both branches cleanly, promoting robust and maintainable error recovery logic.

#include <expected>
#include <iostream>

std::expected<int, std::string> parse_int(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::unexpected("Invalid number format");
    }
}

int main() {
    auto result = parse_int("123");
    
    // Method 1: Check with has_value()
    if (result.has_value()) {
        std::cout << "Parsed: " << result.value() << std::endl;
    }
    
    // Method 2: Use value_or() for default
    int value = parse_int("abc").value_or(0);
    std::cout << "Value or default: " << value << std::endl;
    
    return 0;
}

Output:

Parsed: 123
Value or default: 0

🔹 Chaining Operations

Chaining range operations creates readable pipelines that process data sequentially without intermediate storage. For instance, data | filter(pred) | transform(func) | take(3) filters, transforms, and limits results in one expression. This functional approach enhances clarity and maintainability by separating concerns into distinct steps. It also leverages compiler optimizations for efficient execution. Chaining is central to modern C++ ranges, enabling complex data transformations with minimal code.

#include <expected>
#include <iostream>

std::expected<int, std::string> double_if_positive(int x) {
    if (x > 0) return x * 2;
    return std::unexpected("Number must be positive");
}

std::expected<int, std::string> add_ten(int x) {
    return x + 10;
}

int main() {
    auto result = std::expected<int, std::string>{5}
        .and_then(double_if_positive)
        .and_then(add_ten);
    
    if (result) {
        std::cout << "Final result: " << *result << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }
    
    return 0;
}

Output:

Final result: 20

🧠 Test Your Knowledge

What C++ version introduced std::expected?