Modern C++ Error Handling
Table of contents
Open Table of contents
Part 1: Evolution of Error Handling in C++
Let’s examine different approaches using a simple example function that parses integers:
int parse_int(std::string_view number);
Exception Handling (Classic Approach)
Exceptions provide a way to separate error handling from regular code flow:
int parse_int(std::string_view number) {
if (/* validation fails */)
throw std::runtime_error(std::format("{} is not a number", number));
// Parse and return number
}
void test() {
try {
int value = parse_int("abc");
std::println("Value: {}", value);
}
catch (std::exception& ex) {
std::println("Error: {}", ex.what());
}
}
C++17: std::optional
std::optional provides an elegant way to represent the possibility of a missing value:
std::optional<int> parse_int(std::string_view number) {
if (/* validation fails */)
return {}; // Return empty optional
// Parse and return number
return parsed_value;
}
void test(std::string_view str) {
if (auto oi = parse_int(str))
std::println("Value: {}", *oi);
else
std::println("Failed to parse number");
}
C++23: std::expected
std::expected is similar to Rust’s Result type, allowing you to return either a value or an error:
std::expected<int, std::runtime_error> parse_int(std::string_view number) {
if (/* validation fails */)
return std::unexpected(std::runtime_error("Invalid number format"));
// Parse and return number
return parsed_value;
}
void test(std::string_view str) {
if (auto ei = parse_int(str))
std::println("Value: {}", *ei);
else
std::println("Error: {}", ei.error().what());
}
C++23: Monadic Operations
C++23 introduces monadic operations for std::expected and std::optional, enabling functional-style error handling:
void test(std::string_view str) {
auto ef = parse_int(str)
.transform([](int i) {
return i - 1; // Transform the value
})
.and_then([](int i) -> std::expected<float, std::runtime_error> {
if (i != 0)
return 1.f / i;
else
return std::unexpected(std::runtime_error("divide by zero"));
})
.transform_error([](auto const& ex) {
return std::string(ex.what()); // Transform the error
});
if (ef)
std::println("Result: {}", *ef);
else
std::println("Error: {}", ef.error());
}
Key Monadic Operations
- and_then:
f(v) → E<v, e>- Apply function if value exists, returns another expected/optional - transform:
f(v) → v- Apply function to transform the contained value - or_else:
f(e) → v- Handle the error case - transform_error:
f(e) → e- Transform the error type (only for std::expected)
Part 2: Design for Correctness
Sometimes the best error handling is preventing errors at compile-time or design-time.
Correct by Construction
Create types that can only represent valid states:
struct unchecked {}; // Tag type for unchecked construction
class Month {
int month;
public:
static bool is_valid(int month) {
return month >= 1 && month <= 12;
}
// Safe constructor that validates input
static std::optional<Month> create(int m) {
if (is_valid(m))
return Month(m, unchecked{});
return {};
}
// Unchecked constructor (for internal use)
Month(int month, unchecked): month(month) {
assert(is_valid(month));
}
int value() const { return month; }
};
// Usage
std::string month_as_string(Month m) {
static const std::array<std::string, 12> month_names = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
return month_names[m.value() - 1];
}
Contracts
C++ contracts provide a way to specify preconditions, postconditions, and invariants:
#ifndef NDEBUG
#define pre(expr) do {if(!(expr)) {violation_handler({#expr});}} while(false)
#else
#define pre(expr) do {} while(false)
#endif
struct ViolationInfo {
std::string_view expr;
std::source_location loc = std::source_location::current();
std::string make_message() const {
return std::format("Contract violation: {} at {}:{}",
expr, loc.file_name(), loc.line());
}
};
void default_violation_handler(ViolationInfo const& info) {
std::print("{}", info.make_message());
std::abort();
}
auto violation_handler = &default_violation_handler;
bool is_valid_month(int month) {
return month >= 1 && month <= 12;
}
std::string month_as_string(int month) {
pre(is_valid_month(month));
static const std::array<std::string, 12> month_names = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
return month_names[month - 1];
}
Each approach has its strengths and ideal use cases. Modern C++ gives developers a rich set of tools for handling errors, enabling more expressive and safer code.