C++23中的std::expected:异常处理
众所周知,C++23以前的异常处理是比较麻烦的,尤其是自己要在可能抛出异常的地方,需要自己去捕获它,比如除数为0的异常、使用std::stoi函数将字符串转换成int整型数据、处理文件读写的异常等等,不然很容易造成程序终止。
关于C++23中引入的std::expected,这一全新的词汇表类型,它为函数返回结果的处理提供了一种更加优雅、类型安全的解决方案。
std::expected
是C++23标准库中的一个模板类,定义于头文件<expected>
中。它提供了一种方式来表示两个值之一:类型T
的期望值,或类型E
的非期望值。std::expected
永远不会是无值的。
// Defined in header <expected>
template< class T, class E >
class expected;
(1) (since C++23)
template< class T, class E >requires std::is_void_v<T>
class expected<T, E>;
类模板 std::expected 提供了一种表示两种值的方式:类型为 T 的预期值或类型为 E 的意外值。预期值永远不会是无值的。
- 主模板。在其自身的存储空间中包含预期值或意外值,该存储空间嵌套在预期对象中。
- void 部分特化。表示预期 void 值或包含意外值。如果包含意外值,则嵌套在预期对象中。
如果程序使用引用类型、函数类型或 std::unexpected 的特化来实例化预期值,则程序格式错误。此外,T 不能是 std::in_place_t 或 std::unexpect_t。
模板参数
T - 预期值的类型。该类型必须是(可能为 cv 限定的)void
,或者满足Destructible
即可析构性要求(特别是,不允许使用数组和引用类型)。
E - 意外值的类型。该类型必须满足Destructible
要求,并且必须是 std::unexpected 的有效模板参数(特别是,不允许使用数组、非对象类型和 cv 限定的类型)。
示例程序
#include <cmath>
#include <expected>
#include <iomanip>
#include <iostream>
#include <string_view>enum class parse_error
{invalid_input,overflow
};auto parse_number(std::string_view& str) -> std::expected<double, parse_error>
{const char* begin = str.data();char* end;double retval = std::strtod(begin, &end);if (begin == end)return std::unexpected(parse_error::invalid_input);else if (std::isinf(retval))return std::unexpected(parse_error::overflow);str.remove_prefix(end - begin);return retval;
}int main()
{auto process = [](std::string_view str){std::cout << "str: " << std::quoted(str) << ", ";if (const auto num = parse_number(str); num.has_value())std::cout << "value: " << *num << '\n';// If num did not have a value, dereferencing num// would cause an undefined behavior, and// num.value() would throw std::bad_expected_access.// num.value_or(123) uses specified default value 123.else if (num.error() == parse_error::invalid_input)std::cout << "error: invalid input\n";else if (num.error() == parse_error::overflow)std::cout << "error: overflow\n";elsestd::cout << "unexpected!\n"; // or invoke std::unreachable();};for (auto src : {"42", "42abc", "meow", "inf"})process(src);
}
执行结果如下:
str: "42", value: 42
str: "42abc", value: 42
str: "meow", error: invalid input
str: "inf", error: overflow
油管中TheCherno的一个视频C++ FINALLY Improved Error Handling with std::expected!对于std::exepected
讲解得不错,感兴趣的可以去看看。
- 0:00 - Quick look at std::expected
- 3:30 - Life before std::expected
- 8:07 - Using std::expected for error handling
- 12:25 - Some more useful features of std::expected
- 17:06 - More advanced use case (file reading)
关于std::expected
的代码示例1
#include <iostream>
#include <print>
#include <expected>// https://en.cppreference.com/w/cpp/utility/expected// https://en.cppreference.com/w/cpp/utility/expected/expected// Example of using std::expected in C++23
// This example demonstrates how to use std::expected to handle errors gracefully.
std::expected<int, std::string> divide(int numerator, int denominator) {if (denominator == 0) {return std::unexpected("Division by zero error");}return numerator / denominator;
}int main()
{int a = 10;int b = 0;// Attempt to divide and handle the resultauto result = divide(a, b);if (result) {std::println("Result: {}", *result);} else {std::println("Error: {}", result.error());}// Example with valid divisionb = 2;result = divide(a, b);if (result) {std::println("Result: {}", *result);} else {std::println("Error: {}", result.error());}return 0;
}
运行结果如下:
Error: Division by zero error
Result: 5
关于std::expected
的代码示例2-支持链式调用
#include <iostream>
#include <print>
#include <expected>// https://en.cppreference.com/w/cpp/utility/expected// 这是一个示例程序,演示如何使用 std::expected 进行错误处理。
std::expected<int, std::string> divide(int numerator, int denominator) {if (denominator == 0) {return std::unexpected("Division by zero error");return 0; // Return a default value}return numerator / denominator;
}void test_001()
{auto result = divide(10, 2);if (result) {std::println("Result: {}", *result);} else {std::println("Error: {}", result.error());}
}int test_002()
{auto result = divide(12, 3);result = result.and_then([](int value) { return divide(value, 0); }).or_else([](const std::string& error) {std::println("Error occurred: {}", error);return std::expected<int, std::string>{0};});if (result) {std::println("Final Result: {}", *result);}
}int main()
{// 测试 std::expected 的基本用法test_001();// 测试 std::expected 的链式调用和错误处理test_002();return 0;
}
运行结果如下:
esult: 5
Error occurred: Division by zero error
Final Result: 0
关于std::expected
的代码示例3
#include <iostream>
#include <expected>
#include <string>
#include <print>// https://en.cppreference.com/w/cpp/utility/expected// Example of using std::expected in C++23
// This example demonstrates how to use std::expected to handle errors gracefully.
// 定义一个可能返回int或者字符串错误的expected类型
std::expected<int, std::string> parse_number(const std::string& str) {try {// 尝试将字符串转换为整数return std::stoi(str);} catch (const std::invalid_argument&) {// 如果转换失败,返回一个错误信息return std::unexpected("Invalid number format");} catch (const std::out_of_range&) {// 如果数字超出范围,返回一个错误信息return std::unexpected("Number out of range");}
}int main()
{auto result = parse_number("123");if (result.has_value()) {std::println("Parsed number: {}", *result);} else {std::println("Error: {}", result.error());}result = parse_number("abc");if (result.has_value()) {std::println("Parsed number: {}", *result);} else {std::println("Error: {}", result.error());}result = parse_number("12345678901234567890");if (result.has_value()) {std::println("Parsed number: {}", *result);} else {std::println("Error: {}", result.error());}return 0;
}
运行结果如下:
Parsed number: 123
Error: Invalid number format
Error: Number out of range
总结
C++23引入的std::expected
为函数返回结果的处理提供了一种更加优雅、类型安全的解决方案。它解决了传统错误处理方法中的一些痛点,如类型安全问题、代码可读性问题和性能开销问题等。通过使用std::expected
,开发者可以编写出更加健壮、可维护的代码。在实际开发中,建议开发者积极采用std::expected
来处理函数的返回结果,特别是在对性能和代码质量有较高要求的场景中。当然,注意:如果在c++23标准之前的老项目,可能就不支持std::expected
这种新特性了。
参考资料
- https://en.cppreference.com/w/cpp/utility/expected.html
- C++23 std::expected:一种新的词汇表类型,用于返回函数的结果
- C++23中的新功能之expected和optional