C++ expected | 'Error Handling' Guide

C++ expected | 'Error Handling' Guide

이 글의 핵심

std::expected is a type introduced in C++23 that represents success or error. It explicitly expresses that a function can return a value or an error, allowing error handling without throwing exceptions.

What is expected?

std::expected is a type introduced in C++23 that represents success or error. It explicitly expresses that a function can return a value or an error, allowing error handling without throwing exceptions.

Below is an implementation example using C++. Import necessary modules and perform branching with conditionals. Try running the code directly to check its operation.

#include <expected>

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

Why Needed?:

  • Explicit error handling: Indicate error possibility in function signature
  • Performance: Faster than exceptions (no stack unwinding)
  • Type safety: Check error type at compile time
  • Composable: Chainable with and_then, transform, etc.

Here is detailed implementation code using C++. Ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

// ❌ Exception: Error possibility not shown in signature
int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Cannot divide by zero");
    }
    return a / b;
}
// Caller cannot know exception can be thrown

// ✅ expected: Error possibility explicit
std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return std::unexpected{"Cannot divide by zero"};
    }
    return a / b;
}
// Caller knows error must be handled

expected vs Exception Comparison:

FeatureExceptionstd::expected
Error indication❌ Implicit✅ Explicit
PerformanceSlow (stack unwinding)Fast
Type safety❌ Weak✅ Strong
Chaining❌ Difficult✅ Easy
Use caseExceptional situationsCommon errors

Basic Usage

The following example demonstrates the concept in cpp:

#include <expected>

std::expected<int, std::string> result = divide(10, 2);

// Check success
if (result) {
    std::cout << "Result: " << result.value() << std::endl;
} else {
    std::cout << "Error: " << result.error() << std::endl;
}

Practical Examples

Example 1: File Reading

Here is detailed implementation code using C++. Import necessary modules, ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

#include <expected>
#include <fstream>
#include <string>

enum class FileError {
    NotFound,
    PermissionDenied,
    ReadError
};

std::expected<std::string, FileError> readFile(const std::string& path) {
    std::ifstream file{path};
    
    if (!file) {
        return std::unexpected{FileError::NotFound};
    }
    
    std::string content;
    if (!std::getline(file, content, '\0')) {
        return std::unexpected{FileError::ReadError};
    }
    
    return content;
}

int main() {
    auto result = readFile("data.txt");
    
    if (result) {
        std::cout << "Content: " << *result << std::endl;
    } else {
        switch (result.error()) {
            case FileError::NotFound:
                std::cout << "File not found" << std::endl;
                break;
            case FileError::ReadError:
                std::cout << "Read failed" << std::endl;
                break;
        }
    }
}

Example 2: Chaining

Here is detailed implementation code using C++. Import necessary modules, perform work efficiently through async processing, ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

#include <expected>

std::expected<int, std::string> parseAndValidate(const std::string& str) {
    return parseInt(str)
        .and_then([](int x) -> std::expected<int, std::string> {
            if (x < 0) {
                return std::unexpected{"Negative not allowed"};
            }
            return x;
        })
        .and_then([](int x) -> std::expected<int, std::string> {
            if (x > 100) {
                return std::unexpected{"Exceeds 100"};
            }
            return x;
        });
}

int main() {
    auto result = parseAndValidate("50");
    
    if (result) {
        std::cout << "Valid: " << *result << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }
}

Example 3: Transform

Here is the main implementation:

#include <expected>

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

int main() {
    auto result = divide(10, 2)
        .transform([](int x) { return x * 2; })  // Transform on success
        .or_else([](auto err) {    // Handle error
            std::cout << "Error: " << err << std::endl;
            return std::expected<int, std::string>{0};
        });
    
    std::cout << "Result: " << result.value() << std::endl;
}

Value Access

Here is detailed implementation code using C++. Ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

std::expected<int, std::string> result = divide(10, 2);

// has_value
if (result.has_value()) {
    std::cout << result.value() << std::endl;
}

// bool conversion
if (result) {
    std::cout << *result << std::endl;
}

// value_or
int val = result.value_or(0);

// error
if (!result) {
    std::cout << result.error() << std::endl;
}

Common Issues

Issue 1: Exception

Below is an implementation example using C++. Perform work efficiently through async processing, ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

std::expected<int, std::string> result = divide(10, 0);

// ❌ Exception on error
try {
    int val = result.value();  // std::bad_expected_access
} catch (const std::bad_expected_access<std::string>& e) {
    std::cout << "Error: " << e.error() << std::endl;
}

// ✅ Check before access
if (result) {
    int val = *result;
}

Issue 2: void Type

Here is detailed implementation code using C++. Ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

// Indicate success only
std::expected<void, std::string> execute() {
    if (error) {
        return std::unexpected{"Execution failed"};
    }
    return {};  // Success
}

int main() {
    auto result = execute();
    
    if (result) {
        std::cout << "Success" << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }
}

Exception vs expected

Here is detailed implementation code using C++. Ensure stability through error handling, perform branching with conditionals. Understand the role of each part while examining the code.

// Exception
int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Cannot divide by zero");
    }
    return a / b;
}

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

// expected advantages:
// - Explicit error handling
// - Performance (faster than exceptions)
// - Type safety

Practical Patterns

Pattern 1: Pipeline Processing

The following example demonstrates the concept in cpp:

std::expected<int, std::string> parseInt(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::unexpected{"Parse failed"};
    }
}

std::expected<int, std::string> validateRange(int value) {
    if (value < 0 || value > 100) {
        return std::unexpected{"Out of range: 0-100"};
    }
    return value;
}

std::expected<int, std::string> doubleValue(int value) {
    return value * 2;
}

// Pipeline
auto result = parseInt("50")
    .and_then(validateRange)
    .and_then(doubleValue);

if (result) {
    std::cout << "Result: " << *result << '\n';  // 100
} else {
    std::cout << "Error: " << result.error() << '\n';
}

FAQ

Q1: What is expected?

A: C++23 type that represents success or error. Explicitly expresses that function can return value or error.

Below is an implementation example using C++. Ensure stability through error handling, perform branching with conditionals. Try running the code directly to check its operation.

std::expected<int, std::string> result = divide(10, 2);

if (result) {
    std::cout << "Success: " << *result << '\n';
} else {
    std::cout << "Error: " << result.error() << '\n';
}

Q2: How does it differ from exceptions?

A:

  • Explicit error: Show error type in function signature
  • Performance: Faster than exceptions (no stack unwinding)
  • Type safety: Check error type at compile time
  • Composable: Chainable with and_then, transform, etc.

Q3: How to access value?

A:

  • value(): Access value (exception on error)
  • *: Access value (UB on error)
  • value_or(default): Value or default
  • error(): Access error

Q4: How to chain?

A: Use and_then, transform, or_else.

The following example demonstrates the concept in cpp:

auto result = parseInt("50")
    .and_then([](int x) -> std::expected<int, std::string> {
        if (x < 0) return std::unexpected{"Negative not allowed"};
        return x;
    })
    .transform([](int x) {
        return x * 2;  // Transform on success
    })
    .or_else([](auto err) -> std::expected<int, std::string> {
        std::cerr << "Error: " << err << '\n';
        return 0;  // Default on error
    });

Q5: Is expected<void, E> possible?

A: Yes. Use when indicating success/failure without returning value.

Q6: What about performance?

A: Faster than exceptions. No stack unwinding and inlinable, but return value size increases.

Recommendation: Use expected for common errors, exceptions for exceptional situations


Master error handling with std::expected! 🚀