C++20 标准库中的 std::expected:更安全的错误处理

在 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_thenor_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 或链式调用、编译器支持较新

五、实践建议

  1. 统一错误类型
    对于大型项目,建议定义统一的错误结构体或枚举,并在 std::expected 中使用。例如:

    enum class ErrorCode { NotFound, InvalidInput, Timeout };
    
    struct Error {
        ErrorCode code;
        std::string message;
    };
    
    std::expected<ReturnType, Error> func();
  2. 链式错误处理
    充分利用 and_thenor_else,减少嵌套 if。这不仅提升可读性,也便于单元测试。

  3. 避免过度使用
    在性能极端敏感的低层代码(例如内核或驱动)中,过度使用 std::expected 可能导致额外开销。可根据实际需求灵活选择。

  4. 文档化
    记录哪些函数返回 std::expected,错误码/结构体的语义,以帮助团队成员正确使用。

六、结语

std::expected 为 C++ 引入了一种更安全、更直观的错误处理机制。与传统异常、错误码和 std::optional 相比,它兼具类型安全与丰富的错误信息,尤其适合需要显式错误传播与链式调用的业务逻辑。随着 C++23 的正式发布,std::expected 将成为标准库的一部分,建议在新项目中早期规划使用,并在已有项目中逐步迁移。

发表评论