C++17 中 std::optional 与 std::expected 的区别与适用场景

在 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> 在最坏情况下存储 TE,通常使用 union 与状态位,大小取决于两者的最大值。

在大多数场景下,两者的性能差异可以忽略不计;若 E 较大,std::expected 可能占用更多内存。

5. 代码规范与最佳实践

  1. 选择合适的类型

    • 当你只关心是否存在值,且错误信息不重要时,使用 std::optional
    • 当你需要返回错误码或错误消息,或想让调用者统一处理错误时,使用 std::expected
  2. 避免错误信息丢失

    • 如果使用 std::optional,可以配合 std::variant 或自定义错误容器来补充错误信息。
  3. 与异常配合

    • 对于可能抛异常的函数,先捕获异常再返回 std::expected,将异常信息包装为错误值。
  4. 可读性

    • 对返回值使用 if(auto res = func(); res) 而不是 if(func()),可以直接访问 res.value()res.error()
  5. 模板泛化

    • 在泛型算法中,使用 std::optional 可避免错误传播;使用 std::expected 可让错误链更清晰。

6. 小结

  • `std::optional `:轻量、易用,适合表示可空值。
  • std::expected<T, E>:更丰富的错误信息,适合需要统一错误处理的场景。

在实际项目中,先评估函数是否需要返回错误细节;如果需要,就采用 std::expected;如果仅需要判断成功与否,使用 std::optional 即可。这样既能保持代码简洁,又能让错误信息完整传递,提升可维护性。

发表评论