在 C++20 之前,错误处理往往依赖于异常、错误码或返回结构体等方式,开发者需要在不同情境下手动选择最合适的方法。C++23 将 std::expected 引入标准库,为错误处理提供了一种统一且类型安全的方案。本文将从概念、使用场景、实现细节以及与现有错误处理机制的比较四个方面,深入探讨 std::expected 的意义与实践。
一、概念回顾
std::expected 是一种二元类型,类似于 std::variant,但有更明确的语义:它 要么 包含一个值 T,表示成功;要么 包含一个错误值 E,表示失败。与 std::optional 只关注是否存在值不同,std::expected 明确区分成功与错误,避免了错误值被误认为是有效数据的风险。
#include <expected>
#include <string>
std::expected<int, std::string> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception& e) {
return std::unexpected(std::string(e.what()));
}
}
上述示例返回一个 std::expected,调用者可以直接检查是否成功,然后访问值或错误信息。
二、使用场景
1. 需要返回值且错误信息多样的函数
例如网络请求、文件 I/O、数据库查询等场景,错误类型不止一种,甚至可能是自定义错误结构体。std::expected 能让错误信息保持类型安全,并避免使用错误码与错误信息混合。
2. 需要链式调用的业务逻辑
使用 std::expected 可以轻松实现类似于 Rust 的 Result<T, E> 的链式错误传播,利用 and_then、or_else 等成员函数,减少显式的错误检查代码。
auto result = parse_int("123")
.and_then([](int n){ return n > 0 ? std::expected<double, std::string>{sqrt(n)} : std::unexpected("negative"); })
.and_then([](double d){ return std::expected<std::string, std::string>{std::to_string(d)}; });
if (result) {
std::cout << "Result: " << *result << '\n';
} else {
std::cerr << "Error: " << result.error() << '\n';
}
3. 与异常互补
std::expected 是一种显式错误处理方式,避免了异常抛掷的性能和可读性问题。可以根据项目需求选择使用哪种机制,或者在某些层面使用 std::expected,在更高层抛出异常。
三、实现细节
3.1 关键成员函数
| 成员 | 说明 |
|---|---|
bool operator bool() const; |
检查是否成功 |
T& value(); |
获取成功值(成功时可用,否则抛异常) |
const T& value() const; |
const 版本 |
E& error(); |
获取错误值(失败时可用) |
const E& error() const; |
const 版本 |
std::expected<T, E> and_then(Func f) const; |
成功时调用 f 并返回其结果 |
std::expected<T, E> or_else(Func f) const; |
失败时调用 f 并返回其结果 |
T value_or(T&& default_value) const; |
成功时返回值,否则返回默认值 |
E error_or(E&& default_error) const; |
失败时返回错误,否则返回默认错误 |
3.2 资源管理
std::expected 的实现通常采用内部 std::variant,并借助 std::variant 的移动语义,确保资源安全。构造函数提供 std::in_place_type_t<T> 或 std::in_place_type_t<E> 的 overload,以明确指定存储类型。
3.3 与异常的互操作
std::expected本身不抛异常;访问错误值时可通过std::unexpected抛异常。- 现有异常代码可以轻松转换为 std::expected,例如:
template<typename Func>
auto to_expected(Func&& f) {
try {
return std::expected<decltype(f()), std::exception_ptr>{f()};
} catch (...) {
return std::unexpected(std::current_exception());
}
}
四、与传统错误处理方式比较
| 方式 | 优点 | 缺点 |
|---|---|---|
| 异常 | 代码简洁、错误传播自动 | 运行时成本、异常安全难保证 |
| 错误码 | 性能优越 | 需要手动检查、容易忽略 |
| std::optional | 简单、无错误信息 | 不能表达错误细节 |
| std::expected | 类型安全、表达成功/失败 | 需要使用 if 或链式调用、编译器支持较新 |
五、实践建议
-
统一错误类型
对于大型项目,建议定义统一的错误结构体或枚举,并在 std::expected 中使用。例如:enum class ErrorCode { NotFound, InvalidInput, Timeout }; struct Error { ErrorCode code; std::string message; }; std::expected<ReturnType, Error> func(); -
链式错误处理
充分利用and_then与or_else,减少嵌套if。这不仅提升可读性,也便于单元测试。 -
避免过度使用
在性能极端敏感的低层代码(例如内核或驱动)中,过度使用 std::expected 可能导致额外开销。可根据实际需求灵活选择。 -
文档化
记录哪些函数返回 std::expected,错误码/结构体的语义,以帮助团队成员正确使用。
六、结语
std::expected 为 C++ 引入了一种更安全、更直观的错误处理机制。与传统异常、错误码和 std::optional 相比,它兼具类型安全与丰富的错误信息,尤其适合需要显式错误传播与链式调用的业务逻辑。随着 C++23 的正式发布,std::expected 将成为标准库的一部分,建议在新项目中早期规划使用,并在已有项目中逐步迁移。