C++23 中的 std::expected 用法详解

在 C++23 中加入了一个全新的异常处理工具——std::expected。它的设计灵感来自 Rust 的 Result 类型,旨在让错误处理更加直观、可组合且无运行时开销。下面我们从基本概念、典型用法、与异常比较以及性能评估几个角度,深入探讨 std::expected 的使用。


一、什么是 std::expected?

std::expected<T, E> 是一个模板类型,表示一个可能成功返回值 T 或者失败返回错误类型 E 的“期望”值。它的核心特征有:

成功 失败
存在值 has_value() 返回 truevalue() 访问 T has_value() 返回 falseerror() 访问 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_thentransform

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 的运行时几乎与纯返回值相同,远优于抛异常的情况。只有在真正抛出异常时才会有明显性能下降。


七、实战场景

  1. 文件读取:返回 std::expected<std::string, std::error_code>,错误码可以直接映射到 std::filesystem 的错误。
  2. 网络协议解析:把解析错误(如字段缺失、长度错误)作为错误类型。
  3. 数据库访问:返回查询结果或 SQL 错误信息,避免抛异常导致事务不易恢复。
  4. 图形渲染管线:创建纹理、加载模型时返回 expected,错误信息可直接传递到 UI。

八、总结

std::expected 通过将错误信息与返回值绑定,提供了一种类型安全、零成本且易于组合的错误处理方式。与传统异常相比,它更适用于业务逻辑层,减少了不必要的异常开销和栈展开成本。C++23 的标准库中加入它是一个重要的里程碑,建议在新项目或迁移项目中积极采纳。

提示:在使用 std::expected 时,务必确保错误类型 E 能够被拷贝或移动(最好是 std::error_codestd::string 或自定义结构),并在返回错误前避免使用裸指针或悬空引用。

祝你在 C++ 编程的道路上玩得开心,别忘了给错误也加上“期待”的姿态!

发表评论