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:
| Feature | Exception | std::expected |
|---|---|---|
| Error indication | ❌ Implicit | ✅ Explicit |
| Performance | Slow (stack unwinding) | Fast |
| Type safety | ❌ Weak | ✅ Strong |
| Chaining | ❌ Difficult | ✅ Easy |
| Use case | Exceptional situations | Common 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
Related Articles
- C++ Optional Complete Guide | nullopt·value_or·C++23 Monadic Operations·Performance·Practical Patterns
- C++ Try C++23 Core Features First [#37-1]
- C++ Modern Features | “C++17/20/23” Core Summary
Master error handling with std::expected! 🚀