C++ std::expected作为std::optional的错误处理增强
使用 std::optional 处理可能失败的操作时,虽然能表达“无值”状态,但无法传递“为什么失败”的具体信息。std::expected (C++23) 解决了这一问题,它在同一个对象中封装了预期的成功值或错误对象,兼具 std::optional 的轻量性和异常的信息传递能力。
1. 准备开发环境
在开始编写代码之前,必须确保你的编译器支持 C++23 标准库。
- 确认 编译器版本满足最低要求:
- GCC:版本 12 或更高(需链接
-lstdc++)。 - Clang:版本 16 或更高。
- MSVC (Visual Studio):版本 19.36 (Visual Studio 2022 17.6) 或更高。
- GCC:版本 12 或更高(需链接
- 配置 编译参数,在编译命令中加入
-std=c++23标志。 - 包含 头文件
<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。
- 声明 函数返回类型为
std::expected<期望值类型, 错误类型>。 - 使用
std::unexpected包装错误对象并返回。 - 直接 返回成功值(无需特殊包装)。
下面展示一个除法运算,当除数为零时返回错误字符串。
// 定义错误类型枚举,比裸字符串更规范
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 的函数后,需要检查状态并取出数据。
- 调用 函数并赋值给变量。
- 检查
has_value()方法判断是否成功。 - 使用
value()获取成功值,或error()获取错误对象。 - 利用
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
仅当前一步操作成功时,才执行下一步操作。
- 调用
.and_then()传入一个 lambda。 - 编写 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,即单纯的映射)。
- 调用
.transform()传入 lambda。 - 执行 值的映射操作(例如将 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
仅当前一步失败时,执行错误处理逻辑,允许尝试修复或记录日志。
- 调用
.or_else()传入 lambda。 - 处理 错误对象并返回一个新的
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. 实战案例:配置文件解析器
结合以上知识,编写一个简易的配置解析流程。
- 定义 数据结构
Config和错误枚举ParseError。 - 实现
read_file(模拟读取文件),返回std::expected<std::string, ParseError>。 - 实现
parse_content,返回std::expected<Config, ParseError>。 - 组合 这些函数。
#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 或条件判断中,而主业务流程保持了线性和清晰。

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