如何在 C++20 中使用 std::expected 处理错误?

在 C++23 中正式引入了 std::expected,但在 C++20 之前已经可以通过第三方实现(如 tl::expected)或自己实现一个简化版来获得类似功能。std::expected 提供了一种更安全、更可读的方式来处理可能失败的函数,而不是传统的异常或错误码。下面我们从概念、实现、使用示例以及与异常的比较四个方面详细阐述。

1. 概念与设计目标

  • 成功值(Success)std::expected<T, E> 包含一个成功值 T 或错误值 E
  • 无错误值:与 `std::optional ` 类似,只是 `std::optional` 只表示存在或不存在一个值,而 `std::expected` 更细致,能同时给出错误原因。
  • 避免异常:对于需要频繁返回错误或在性能敏感路径中,使用 expected 可以减少异常的开销。
  • 可组合:可以轻松链式调用多个返回 expected 的函数,利用 operator>>.and_then() 进行错误传播。

2. 简化版实现(C++20 语法)

#include <variant>
#include <utility>
#include <iostream>
#include <string>

template<class T, class E>
class expected {
public:
    // 构造成功值
    expected(const T& val) : value_(val) {}
    expected(T&& val) : value_(std::move(val)) {}

    // 构造错误值
    expected(const E& err) : value_(err) {}
    expected(E&& err) : value_(std::move(err)) {}

    // 判断是否成功
    explicit operator bool() const noexcept {
        return std::holds_alternative <T>(value_);
    }

    // 访问成功值(未检查)
    const T& value() const & { return std::get <T>(value_); }
    T& value() & { return std::get <T>(value_); }
    const T&& value() const && { return std::get <T>(std::move(value_)); }
    T&& value() && { return std::get <T>(std::move(value_)); }

    // 访问错误值(未检查)
    const E& error() const & { return std::get <E>(value_); }
    E& error() & { return std::get <E>(value_); }
    const E&& error() const && { return std::get <E>(std::move(value_)); }
    E&& error() && { return std::get <E>(std::move(value_)); }

private:
    std::variant<T, E> value_;
};

注意:上面示例仅展示核心功能,缺少诸如 and_then, transform, value_or 等实用成员。实际项目请使用成熟库或自行扩展。

3. 使用示例

假设我们实现一个简单的文件读取函数 read_file,返回内容或错误信息:

expected<std::string, std::string> read_file(const std::string& path) {
    std::ifstream in(path);
    if (!in.is_open()) {
        return expected<std::string, std::string>("无法打开文件:" + path);
    }
    std::stringstream buffer;
    buffer << in.rdbuf();
    return expected<std::string, std::string>(buffer.str());
}

调用者可以链式处理:

auto result = read_file("example.txt");
if (result) {
    std::cout << "文件内容长度: " << result.value().size() << '\n';
} else {
    std::cerr << "读取失败: " << result.error() << '\n';
}

如果我们需要将读取结果进一步解析为 JSON,可以使用 and_then

auto json_result = read_file("config.json")
    .and_then([](std::string content) -> expected<nlohmann::json, std::string> {
        try {
            return expected<nlohmann::json, std::string>(nlohmann::json::parse(content));
        } catch (const std::exception& e) {
            return expected<nlohmann::json, std::string>("JSON 解析错误: " + std::string(e.what()));
        }
    });

if (json_result) {
    // 成功
} else {
    std::cerr << json_result.error() << '\n';
}

提示:如果你使用的是 C++23 标准库的 std::expected,上述链式调用将更简洁,例如 read_file(...).transform(...).and_then(...)

4. 与异常的比较

方面 expected 异常
错误传播 通过返回值链式传播 通过抛异常自动传播
性能 适用于高频路径,无抛异常开销 抛异常在异常路径会产生堆栈展开成本
可读性 明确指出函数可能失败 需要查看调用栈或异常处理逻辑
类型安全 明确返回值类型 异常类型可多样但不强制
适用场景 IO、解析、算法错误等 需要中断执行、资源回收等

5. 进阶技巧

  • 自定义错误类型:使用 enum class ErrorCode 或结构体来统一错误码,方便调试与日志。
  • 宏化错误生成:定义 MAKE_ERROR(msg) 宏,简化错误值构造。
  • std::optional 混用:在函数既可能返回值也可能返回空值时,可以先返回 `std::optional `,再包装为 `expected`。

6. 小结

std::expected 在 C++23 中正式标准化,它为错误处理提供了一种更直观、类型安全、无异常的方案。即便在 C++20 环境下,通过第三方实现或自定义简化版也能大幅提升代码可读性与错误传播效率。建议在项目中逐步引入 expected,替代传统的错误码或异常方式,尤其是在性能敏感或多路径错误处理场景中。


发表评论