在 C++17 之后,标准库提供了 std::optional 用于表示可能存在或不存在的值;而在 C++23 中新增了 std::expected,用于更直观地表达成功或错误的结果。它们在语义、使用方式和适用场景上有着明显的差别。下面从概念、实现细节、错误处理、性能和最佳实践四个方面对比这两种类型,并给出具体的使用示例。
1. 基本概念
| 类型 | 语义 | 主要用途 |
|---|---|---|
| `std::optional | ||
| ` | 表示某个值可能不存在。 | 处理可空值、延迟计算、可选参数等。 |
std::expected<T, E> |
表示一个成功结果 T 或一个错误 E。 |
统一错误处理,避免异常/错误码混用。 |
std::optional只关心是否有值;若无值,则没有任何错误信息。std::expected关注错误类型,允许携带错误码、错误信息等。
2. 语法与实现细节
2.1 std::optional
#include <optional>
std::optional <int> maybe_divide(int a, int b) {
if (b == 0) return std::nullopt;
return a / b;
}
has_value()/operator bool()判断是否存在值。value()或*opt获取值;若无值则抛std::bad_optional_access。- 需要 `#include `。
2.2 std::expected
#include <expected>
std::expected<int, std::string> safe_divide(int a, int b) {
if (b == 0) return std::unexpected<std::string>("division by zero");
return a / b;
}
value()返回成功值;error()返回错误对象。has_value()与operator bool()判断是否成功。- 可以使用
std::unexpected包装错误。 - `#include `。
3. 错误处理方式
| 特点 | std::optional |
std::expected |
|---|---|---|
| 错误信息 | 无 | 可以携带 |
| 处理方式 | if(opt) |
if(res) 或 res.error() |
| 与异常的关系 | 与异常无直接关联 | 可与异常结合,例如 throw std::runtime_error(...) |
| 适合场景 | 只需知道是否有值 | 需要返回具体错误码/信息 |
示例:文件读取
// 只需知道文件是否打开
std::optional<std::ifstream> open_file(const std::string& name) {
std::ifstream f(name);
if (!f.is_open()) return std::nullopt;
return f;
}
// 返回错误码
std::expected<std::ifstream, std::string> open_file2(const std::string& name) {
std::ifstream f(name);
if (!f.is_open()) return std::unexpected<std::string>("file not found");
return f;
}
4. 性能与内存
- **`std::optional `** 对于 trivially copyable 的 `T`,实现通常是堆栈存放,大小为 `sizeof(T) + 1`(布尔位)。
std::expected<T, E>在最坏情况下存储T或E,通常使用union与状态位,大小取决于两者的最大值。
在大多数场景下,两者的性能差异可以忽略不计;若 E 较大,std::expected 可能占用更多内存。
5. 代码规范与最佳实践
-
选择合适的类型
- 当你只关心是否存在值,且错误信息不重要时,使用
std::optional。 - 当你需要返回错误码或错误消息,或想让调用者统一处理错误时,使用
std::expected。
- 当你只关心是否存在值,且错误信息不重要时,使用
-
避免错误信息丢失
- 如果使用
std::optional,可以配合std::variant或自定义错误容器来补充错误信息。
- 如果使用
-
与异常配合
- 对于可能抛异常的函数,先捕获异常再返回
std::expected,将异常信息包装为错误值。
- 对于可能抛异常的函数,先捕获异常再返回
-
可读性
- 对返回值使用
if(auto res = func(); res)而不是if(func()),可以直接访问res.value()或res.error()。
- 对返回值使用
-
模板泛化
- 在泛型算法中,使用
std::optional可避免错误传播;使用std::expected可让错误链更清晰。
- 在泛型算法中,使用
6. 小结
- `std::optional `:轻量、易用,适合表示可空值。
std::expected<T, E>:更丰富的错误信息,适合需要统一错误处理的场景。
在实际项目中,先评估函数是否需要返回错误细节;如果需要,就采用 std::expected;如果仅需要判断成功与否,使用 std::optional 即可。这样既能保持代码简洁,又能让错误信息完整传递,提升可维护性。