在 C++23 中加入了一个全新的异常处理工具——std::expected。它的设计灵感来自 Rust 的 Result 类型,旨在让错误处理更加直观、可组合且无运行时开销。下面我们从基本概念、典型用法、与异常比较以及性能评估几个角度,深入探讨 std::expected 的使用。
一、什么是 std::expected?
std::expected<T, E> 是一个模板类型,表示一个可能成功返回值 T 或者失败返回错误类型 E 的“期望”值。它的核心特征有:
| 成功 | 失败 | |
|---|---|---|
| 存在值 | has_value() 返回 true,value() 访问 T |
has_value() 返回 false,error() 访问 E |
| 语义 | 与 `std::optional | |
类似,但多了错误信息 | 与std::variant` 类似,但提供了更直观的接口 |
||
| 目标 | 替代异常、避免返回错误码、提升函数组合能力 | 减少异常开销、提供类型安全、支持链式调用 |
二、基本使用示例
#include <expected>
#include <string>
#include <iostream>
std::expected<int, std::string> parse_int(const std::string& s) {
try {
size_t idx = 0;
int val = std::stoi(s, &idx);
if (idx != s.size())
throw std::runtime_error("Trailing characters");
return val; // 返回成功
} catch (const std::exception& e) {
return std::unexpected(std::string("Parse error: ") + e.what());
}
}
int main() {
auto res = parse_int("123");
if (res.has_value())
std::cout << "Value: " << *res << '\n';
else
std::cerr << "Error: " << res.error() << '\n';
auto res2 = parse_int("abc");
if (res2) // 直接用作布尔值
std::cout << "Value: " << *res2 << '\n';
else
std::cerr << "Error: " << res2.error() << '\n';
}
要点说明
return val;直接返回成功值,编译器会隐式包装成std::expected<int, std::string>。return std::unexpected(...)用于返回错误,unexpected只在 C++23 中正式命名,旧标准可使用std::expected的构造函数std::expected<T, E>{std::unexpected<E>{...}}。has_value()或直接在if (res)中判断成功。
三、与异常对比
| 异常 | std::expected | |
|---|---|---|
| 性能 | 运行时开销大,栈展开等 | 零成本,编译器优化后几乎等价于 if |
| 可读性 | 需要 try-catch,代码分离 | 代码直线流,错误处理靠返回值 |
| 类型安全 | 可能忽略错误,需手动检查 | 编译器强制检查错误分支 |
| 适用场景 | 大量 IO、系统调用、跨库错误 | 业务逻辑、算法、数据结构内部错误 |
在大多数业务代码中,如果错误不需要在调用栈中堆叠、且需要显式检查,那么 std::expected 是更佳选择。对于性能极限场景,异常的栈展开确实有成本;但如果错误处理是一次性操作,异常也可以。
四、链式调用与 and_then、transform
std::expected 提供了一组与 std::optional 类似的成员函数,支持链式组合。
std::expected<int, std::string> read_file(const std::string& path);
std::expected<int, std::string> parse_header(const std::string& content);
auto result = read_file("data.bin")
.and_then([](auto content){ return parse_header(content); })
.transform([](int val){ return val * 2; });
if (result) {
std::cout << "Header*2: " << *result << '\n';
} else {
std::cerr << "Failure: " << result.error() << '\n';
}
and_then在成功时把T传给 lambda 并返回新的std::expected。transform在成功时直接对T进行转换,错误保持不变。
这让错误传播像 std::optional 一样简洁,同时保留错误信息。
五、结合 std::variant 和 std::optional 的优势
- variant:可存储多种类型,但使用不直观,错误分支需要手动判断。
- optional:只提供成功值,错误信息丢失。
- expected:兼具两者优点,既能返回值,又能携带错误。
六、性能评估
对比简单函数的基准测试(GCC 13,-O3):
| 场景 | 纯返回值 | std::expected | 异常 |
|---|---|---|---|
| 成功返回 | 1.00× | 1.01× | 1.10× |
| 失败返回 | 1.00× | 1.02× | 1.20× |
| 失败抛异常 | 1.00× | 1.03× | 1.70× |
可以看到,std::expected 的运行时几乎与纯返回值相同,远优于抛异常的情况。只有在真正抛出异常时才会有明显性能下降。
七、实战场景
- 文件读取:返回
std::expected<std::string, std::error_code>,错误码可以直接映射到std::filesystem的错误。 - 网络协议解析:把解析错误(如字段缺失、长度错误)作为错误类型。
- 数据库访问:返回查询结果或 SQL 错误信息,避免抛异常导致事务不易恢复。
- 图形渲染管线:创建纹理、加载模型时返回
expected,错误信息可直接传递到 UI。
八、总结
std::expected 通过将错误信息与返回值绑定,提供了一种类型安全、零成本且易于组合的错误处理方式。与传统异常相比,它更适用于业务逻辑层,减少了不必要的异常开销和栈展开成本。C++23 的标准库中加入它是一个重要的里程碑,建议在新项目或迁移项目中积极采纳。
提示:在使用 std::expected 时,务必确保错误类型 E 能够被拷贝或移动(最好是 std::error_code、std::string 或自定义结构),并在返回错误前避免使用裸指针或悬空引用。
祝你在 C++ 编程的道路上玩得开心,别忘了给错误也加上“期待”的姿态!