std::expected (C++23)
Type-safe error handling without exceptions
The Error Handling Dilemma
Exceptions
- ✓ Clean error propagation
- ✓ Type-safe
- ✗ Runtime overhead
- ✗ Hidden control flow
- ✗ Not deterministic
Error Codes
- ✓ Predictable performance
- ✓ Explicit control flow
- ✗ Easy to ignore
- ✗ No type information
- ✗ Manual propagation
C++23: std::expected
std::expected<T, E> represents either a value of type T (success) or an error of type E (failure).
#include <expected>
#include <string>
#include <iostream>
// Function that might fail
std::expected<double, std::string> safe_divide(double a, double b) {
if (b == 0.0) {
return std::unexpected("Division by zero");
}
if (std::isnan(a) || std::isnan(b)) {
return std::unexpected("NaN argument");
}
return a / b;
}
// Usage
int main() {
auto result = safe_divide(10.0, 2.0);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Error: " << result.error() << std::endl;
}
}Basic Operations
std::expected<int, std::string> get_value(bool success) {
if (success) return 42;
return std::unexpected("Failed");
}
auto result = get_value(true);
// Check state
bool has_value = result.has_value(); // true
// Access value (undefined behavior if empty!)
int value = result.value(); // 42
int v2 = *result; // 42 (dereference)
// Safe access with default
int v3 = result.value_or(0); // 42 (or 0 if error)
// Access error (only valid if has_value() == false)
auto error = result.error(); // UB here since successMonadic Operations
Chain operations that might fail without explicit error checking.
1. transform
// transform: Apply function to value if present
std::expected<int, std::string> parse_number(const std::string& s);
auto result = parse_number("42")
.transform([](int n) { return n * 2; }) // 84
.transform([](int n) { return std::to_string(n); }); // "84"
// If parse_number failed, transforms are skipped
// Error propagates through the chain2. and_then
// and_then: Chain operations that return expected
std::expected<int, std::string> parse_int(const std::string& s);
std::expected<double, std::string> divide(double a, double b);
auto result = parse_int("100")
.and_then([](int n) { return divide(n, 2.0); })
.and_then([](double d) { return divide(d, 5.0); });
// Result: 10.0 or error from any step3. or_else
// or_else: Error recovery
std::expected<int, std::string> load_config(const std::string& path);
auto config = load_config("config.json")
.or_else([](const std::string& error) {
std::cerr << "Failed to load: " << error << std::endl;
return load_config("config.default.json");
})
.or_else([](const std::string&) {
return std::expected<int, std::string>{42}; // Use default
});Practical Example: File Parser
enum class ParseError {
FileNotFound,
InvalidFormat,
OutOfRange
};
struct Config {
int port;
std::string host;
bool use_ssl;
};
std::expected<Config, ParseError> load_config(const std::string& path) {
return read_file(path)
.and_then(parse_json)
.and_then(validate_config)
.transform([](const json& j) {
return Config{
.port = j["port"],
.host = j["host"],
.use_ssl = j.value("ssl", false)
};
});
}
// Usage with pattern matching (C++23)
auto result = load_config("app.conf");
if (result) {
const auto& config = *result;
std::println("Port: {}", config.port);
} else {
switch (result.error()) {
case ParseError::FileNotFound:
std::println("Config file not found");
break;
case ParseError::InvalidFormat:
std::println("Invalid config format");
break;
// ...
}
}expected vs optional
| Feature | std::optional<T> | std::expected<T, E> |
|---|---|---|
| Represents | Value or nothing | Value or error |
| Error Info | No (just empty) | Yes (typed error) |
| Use Case | Nullable values, find operations | Fallible operations |
| Monadic Ops | and_then, transform, or_else | Same + error() accessor |
Best Practices
- ✓Use
expectedfor operations that can fail with specific errors - ✓Use monadic operations to build error-handling pipelines
- ✓Choose meaningful error types (enums for simple cases, classes for rich info)
- ✗Don't use
expectedfor truly exceptional circumstances (use exceptions) - ✗Avoid
.value()without checking first (usevalue_oror checkhas_value())