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