文章目录

C++ std::expected作为std::optional的错误处理增强

发布于 2026-04-25 21:24:24 · 浏览 6 次 · 评论 0 条

C++ std::expected作为std::optional的错误处理增强

使用 std::optional 处理可能失败的操作时,虽然能表达“无值”状态,但无法传递“为什么失败”的具体信息。std::expected (C++23) 解决了这一问题,它在同一个对象中封装了预期的成功值或错误对象,兼具 std::optional 的轻量性和异常的信息传递能力。


1. 准备开发环境

在开始编写代码之前,必须确保你的编译器支持 C++23 标准库。

  1. 确认 编译器版本满足最低要求:
    • GCC:版本 12 或更高(需链接 -lstdc++)。
    • Clang:版本 16 或更高。
    • MSVC (Visual Studio):版本 19.36 (Visual Studio 2022 17.6) 或更高。
  2. 配置 编译参数,在编译命令中加入 -std=c++23 标志。
  3. 包含 头文件 <expected>
#include <expected>
#include <iostream>
#include <string>
#include <vector>

2. 理解核心概念:Optional 与 Expected 的区别

std::expected 本质上是一个“持有值”或“持有错误”的变体。为了更直观地理解两者的区别,请参考下表。

特性 std::optional std::expected
状态含义 有值 或 无值 有值 或 有错误
错误信息 无法携带错误原因 携带具体的错误对象
典型用途 可选参数、查找未命中 可能失败的运算、I/O 操作
类型签名 optional<T> expected<T, E>

3. 定义返回 expected 的函数

通过定义一个返回 std::expected 的函数,替换传统的抛出异常或返回 std::optional

  1. 声明 函数返回类型为 std::expected<期望值类型, 错误类型>
  2. 使用 std::unexpected 包装错误对象并返回。
  3. 直接 返回成功值(无需特殊包装)。

下面展示一个除法运算,当除数为零时返回错误字符串。

// 定义错误类型枚举,比裸字符串更规范
enum class MathError {
    DivByZero,
    NegativeRoot
};

// 返回类型:成功时是 double,失败时是 MathError
std::expected<double, MathError> safe_divide(double a, double b) {
    if (b == 0.0) {
        // 使用 std::unexpected 包装错误
        return std::unexpected(MathError::DivByZero);
    }
    // 直接返回计算结果
    return a / b;
}

4. 检查与提取结果

调用返回 std::expected 的函数后,需要检查状态并取出数据。

  1. 调用 函数并赋值给变量。
  2. 检查 has_value() 方法判断是否成功。
  3. 使用 value() 获取成功值,或 error() 获取错误对象。
  4. 利用 value_or() 提供默认值(仅当错误类型与值类型兼容时适用,但通常不推荐在严格错误处理中混用)。
void process_division() {
    auto result = safe_divide(10.0, 0.0);

    if (result.has_value()) {
        std::cout << "计算结果: " << result.value() << std::endl;
    } else {
        // 获取错误对象
        MathError err = result.error();
        if (err == MathError::DivByZero) {
            std::cout << "错误:除数不能为零" << std::endl;
        }
    }
}

5. 使用函数式风格进行链式调用

std::expected 最强大的功能之一是支持函数式编程风格。你可以将多个可能失败的操作串联起来,避免深层嵌套的 if-else 语句。

5.1 使用 and_then

仅当前一步操作成功时,才执行下一步操作。

  1. 调用 .and_then() 传入一个 lambda。
  2. 编写 lambda 内部逻辑,返回一个新的 std::expected
// 将平方根操作封装
std::expected<double, MathError> safe_sqrt(double x) {
    if (x < 0.0) {
        return std::unexpected(MathError::NegativeRoot);
    }
    return std::sqrt(x);
}

void chaining_example() {
    // 逻辑:(10.0 / 2.0) 的结果开平方
    auto result = safe_divide(10.0, 2.0)
        .and_then([](double quotient) {
            // 这里的 quotient 是 safe_divide 的成功结果
            return safe_sqrt(quotient);
        });

    if (result) {
        std::cout << "链式调用结果: " << result.value() << std::endl;
    } else {
        std::cout << "链式调用发生错误" << std::endl;
    }
}

5.2 使用 transform

仅当前一步成功时,对值进行转换(转换后的类型必须不是 std::expected,即单纯的映射)。

  1. 调用 .transform() 传入 lambda。
  2. 执行 值的映射操作(例如将 double 转为 int)。
auto result2 = safe_divide(10.0, 4.0)
    .transform([](double val) {
        // 将 double 四舍五入转为 int
        return static_cast<int>(std::round(val));
    });
// result2 的类型为 std::expected<int, MathError>

5.3 使用 or_else

仅当前一步失败时,执行错误处理逻辑,允许尝试修复或记录日志。

  1. 调用 .or_else() 传入 lambda。
  2. 处理 错误对象并返回一个新的 std::expected(通常用于恢复默认值或重新抛出)。
auto result3 = safe_divide(10.0, 0.0)
    .or_else([](MathError err) {
        std::cout << "捕获错误,正在尝试修复..." << std::endl;
        // 修复逻辑:返回一个默认的 expected 值
        return std::expected<double, MathError>{1.0};
    });
// 结果为 1.0,而不是错误

6. 实战案例:配置文件解析器

结合以上知识,编写一个简易的配置解析流程。

  1. 定义 数据结构 Config 和错误枚举 ParseError
  2. 实现 read_file(模拟读取文件),返回 std::expected<std::string, ParseError>
  3. 实现 parse_content,返回 std::expected<Config, ParseError>
  4. 组合 这些函数。
#include <expected>
#include <string>
#include <iostream>

struct Config {
    int timeout;
    std::string address;
};

enum class ParseError {
    OpenFailed,
    InvalidFormat
};

// 模拟读取文件
std::expected<std::string, ParseError> read_file(const std::string& path) {
    if (path.empty()) {
        return std::unexpected(ParseError::OpenFailed);
    }
    return "timeout=30\naddress=127.0.0.1";
}

// 模拟解析内容
std::expected<Config, ParseError> parse_content(const std::string& content) {
    if (content.find("timeout=") == std::string::npos) {
        return std::unexpected(ParseError::InvalidFormat);
    }
    Config cfg{30, "127.0.0.1"};
    return cfg;
}

void load_config(const std::string& path) {
    auto config = read_file(path)
        .and_then([](const std::string& content) {
            return parse_content(content);
        })
        .transform([](const Config& cfg) {
            std::cout << "配置加载成功:" << std::endl;
            std::cout << "Address: " << cfg.address << std::endl;
            return cfg.timeout; 
        });

    if (!config) {
        if (config.error() == ParseError::OpenFailed) {
            std::cout << "无法打开文件" << std::endl;
        } else if (config.error() == ParseError::InvalidFormat) {
            std::cout << "文件格式错误" << std::endl;
        }
    } else {
         std::cout << "Timeout: " << config.value() << std::endl;
    }
}

通过这种方式,错误处理逻辑被清晰地分离在 or_else 或条件判断中,而主业务流程保持了线性和清晰。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文