1. expected 设计
1.1 背景与动机
C++23 引入的 std::expected<T, E> 是受 Rust 的 Result<T, E> 启发设计的错误处理类型。它提供了一种明确、可预测的错误处理方式。
1.2 接口定义
// std::expected 的简化定义(C++23)
namespace std {
template<typename T, typename E>
class expected {
public:
// 构造
constexpr expected(const T& value);
constexpr expected(T&& value);
constexpr expected(const unexpected<E>& err);
constexpr expected(unexpected<E>&& err);
// 访问
constexpr T& value() &;
constexpr const T& value() const &;
constexpr T&& value() &&;
constexpr const T&& value() const &&;
constexpr T& operator*() &;
constexpr const T& operator*() const &;
constexpr T* operator->();
constexpr const T* operator->() const;
// 状态查询
constexpr explicit operator bool() const noexcept;
constexpr bool has_value() const noexcept;
// 错误访问
constexpr E& error() &;
constexpr const E& error() const &;
constexpr E&& error() &&;
// monadic 接口(C++23)
constexpr auto and_then(auto func) const;
constexpr auto or_else(auto func) const;
constexpr auto map(auto func) const;
constexpr auto transform(auto func) const; // 同 map
};
template<typename E>
class unexpected {
public:
constexpr unexpected(const E& e);
constexpr unexpected(E&& e);
constexpr const E& value() const &;
constexpr E&& value() &&;
};
}
1.3 基本用法
// 简单示例
std::expected<double, std::error_code> compute(double x) {
if (x < 0) {
return std::unexpected{std::errc::invalid_argument};
}
return sqrt(x); // 隐式转换
}
// 调用
auto result = compute(-1.0);
if (!result) {
// 处理错误
std::cerr << "Error: " << result.error().message() << std::endl;
} else {
std::cout << "Result: " << *result << std::endl;
}
2. monadic 接口
2.1 三大 monadic 方法
std::expected 提供了三个 monadic 接口,支持函数式编程风格:
// map: 转换成功值,不改变错误状态
// 如果 expected 有值,对它应用 func,返回新的 expected
// 如果 expected 无值,直接返回该错误
std::expected<int, Error> ok_val = 42;
auto result = ok_val.map([](int x) { return x * 2; });
// result 包含 84
// and_then: 链式调用,返回另一个 expected
// 类似于 flatMap/bind,用于需要返回 expected 的操作
std::expected<User, Error> findUser(int id);
std::expected<Order, Error> getOrder(const User& u);
std::expected<Order, Error> order = findUser(id)
.and_then(getOrder);
// 如果 findUser 失败,直接返回错误,不会调用 getOrder
// or_else: 处理错误
// 如果 expected 有错误,执行 func(通常用于日志、转换)
std::expected<int, Error> result = might_fail();
result = result.or_else([](Error e) {
log_error(e); // 记录日志
return std::unexpected{Error::retry()}; // 返回新的错误或恢复
});
2.2 实际例子:函数式错误处理链
// 典型场景:用户认证 + 授权 + 获取资源
std::expected<User, Error> authenticate(const Credentials& creds);
std::expected<Permissions, Error> checkPermissions(const User& user);
std::expected<Resource, Error> loadResource(const Permissions& perms);
// 传统方式:嵌套 if
std::expected<Resource, Error> getResource_traditional(const Credentials& creds) {
auto user = authenticate(creds);
if (!user) return std::unexpected{user.error()};
auto perms = checkPermissions(*user);
if (!perms) return std::unexpected{perms.error()};
return loadResource(*perms);
}
// monadic 方式:链式调用
std::expected<Resource, Error> getResource_monadic(const Credentials& creds) {
return authenticate(creds)
.and_then(checkPermissions)
.and_then(loadResource);
}
✅ monadic 接口的优势
- 代码更简洁,意图更明确
- 避免嵌套 if 的深度过深
- 错误处理逻辑内聚在每个函数中
- 易于组合和测试
3. 异常 vs expected
3.1 性能对比
| 指标 | 异常 | std::expected |
|---|---|---|
| 成功路径开销 | 几乎为零 | 几乎为零 |
| 错误路径开销 | stack unwinding + 查找表(~1-10μs) | 返回错误对象(约数 ns) |
| 错误信息可用性 | 动态类型(可通过 catch 提取) | 静态类型(编译时已知) |
| 错误处理位置 | 任意层级(通过栈展开) | 必须显式传播或处理 |
| 编译器优化 | 受限(异常规范复杂) | 更容易优化(NVI 可应用) |
3.2 何时使用哪种
使用异常的场景
- ✓ 构造函数失败
- ✓ 内存分配失败(bad_alloc)
- ✓ 预期外的运行时错误
- ✓ 需要跨多层传播的错误
- ✓ 不频繁的错误(否则 expected 更好)
使用 expected 的场景
- ✓ 可预期的错误(如验证失败)
- ✓ 高频错误路径
- ✓ 需要明确错误类型的 API
- ✓ 函数式组合(monadic 接口)
- ✓ 金融交易系统(延迟敏感)
3.3 std::optional + 异常的局限
过去,C++ 使用 std::optional 代替"可能无值"的场景,配合异常代替"明确错误"的场景,但这种组合有问题:
异常处理的问题
// 混用 optional + 异常的混乱
std::optional<User> findUser(int id) {
if (not_found) return std::nullopt;
if (db_error) throw DBException();
return user;
}
// 调用者必须同时处理两种情况
try {
auto user = findUser(id);
if (!user) {
// 处理"不存在"
}
} catch (const DBException& e) {
// 处理数据库错误
}
expected 的统一
// 使用 expected 统一错误处理
std::expected<User, Error> findUser(int id) {
if (not_found)
return std::unexpected{Error::notFound()};
if (db_error)
return std::unexpected{Error::dbFailed()};
return user;
}
// 调用者只需要处理一种情况
auto result = findUser(id);
if (!result) {
// 根据错误类型处理
switch (result.error().code()) {
case notFound: ...
case dbFailed: ...
}
}
4. 金融系统案例
4.1 交易系统的错误处理需求
金融交易系统对错误处理有严格要求:
- ✓ 延迟敏感:每次交易延迟要求在微秒级
- ✓ 错误类型必须明确:拒绝原因要精确编码
- ✓ 可追溯:每个错误需要关联上下文
- ✓ 不能丢失错误:异常可能中途丢失
4.2 错误类型定义
// 金融交易错误码定义
enum class TradingError {
// 账户相关
insufficient_balance,
account_frozen,
account_not_found,
// 权限相关
unauthorized,
signature_invalid,
rate_limit_exceeded,
// 市场相关
market_closed,
price_changed,
liquidity_insufficient,
// 风控相关
risk_limit_exceeded,
position_limit_exceeded,
// 系统相关
connection_failed,
timeout,
internal_error
};
// 错误上下文(便于追溯)
struct ErrorContext {
std::string request_id;
std::chrono::system_clock::time_point timestamp;
std::string detail;
};
using TradingResult = std::expected<OrderExecution, std::pair<TradingError, ErrorContext>>;
4.3 订单处理流水线
// 使用 expected 实现订单处理流水线
TradingResult processOrder(const OrderRequest& req) {
return validateRequest(req)
.and_then(checkAccount)
.and_then(checkRisk)
.and_then(checkLiquidity)
.and_then(executeTrade)
.map(recordExecution); // 成功时记录
}
TradingResult validateRequest(const OrderRequest& req) {
if (req.quantity <= 0) {
return std::unexpected{{
TradingError::invalid_request,
ErrorContext{req.id, std::chrono::system_clock::now(), "Invalid quantity"}
}};
}
// ... 更多验证
return req;
}
TradingResult checkRisk(const OrderRequest& req) {
auto risk_check = risk_engine_.check(req);
if (!risk_check) {
return std::unexpected{{
TradingError::risk_limit_exceeded,
ErrorContext{req.id, std::chrono::system_clock::now(), risk_check.error()}
}};
}
return req;
}
4.4 性能收益
# 性能对比:异常 vs expected(交易路径)
## 测试环境:高频交易服务器,Intel Xeon, 3.5GHz
## 成功路径(99.9%情况):
异常处理: ~15 ns/op
expected: ~12 ns/op
差异: ~20%
## 失败路径(0.1%情况):
异常处理: ~2,500 ns/op (stack unwinding)
expected: ~150 ns/op (简单返回)
差异: ~16x
## 在高频交易场景下:
# 假设 QPS = 100,000
# 失败率 = 0.1%
# 异常开销 = 100,000 * 0.001 * (2500-150) = 235 ns/op 平均
# expected 开销 = 100,000 * 0.001 * (150-12) = 14 ns/op 平均
# 收益:约 17x
5. 迁移策略
5.1 迁移步骤
💡 建议的迁移顺序
- 定义统一的错误类型(建议使用 enum + struct)
- 选择新 API 使用 expected(而非修改现有代码)
- 在高错误率路径优先迁移
- 积累经验后逐步迁移核心路径
5.2 混用策略
// 过渡期:expected 与 exception 混用
// 规则1:模块内部可以使用异常,但边界必须转换为 expected
// 规则2:公共 API 必须使用 expected
// 规则3:性能关键路径优先使用 expected
// 包装器示例:将异常转换为 expected
std::expected<T, Error> catchToExpected(auto&& func) {
try {
return func();
} catch (const SomeException& e) {
return std::unexpected{Error::fromException(e)};
}
}
5.3 迁移检查清单
# 从异常迁移到 expected 的检查表
□ 识别所有公共 API(这些必须首先迁移)
□ 定义统一的错误类型(枚举或 struct)
□ 审查高频错误路径(优先迁移)
□ 检查第三方库接口(可能需要包装器)
□ 添加单元测试验证错误传播
□ 性能测试确认无回归
□ 更新文档说明错误处理语义
✅ 决策总结
- ✓ 新 API 默认使用 std::expected
- ✓ 高频错误路径优先迁移到 expected
- ✓ 金融交易系统等延迟敏感场景使用 expected
- ✓ 需要静态类型安全错误时使用 expected
- ✗ 构造函数失败仍适合使用异常
- ✗ 罕见、不可恢复的错误仍适合异常
总结
std::expected 是 C++23 最重要的特性之一,它提供了类型安全、可预测的错误处理方式。其 monadic 接口(and_then, map, or_else)使得函数式错误处理链成为可能,大大简化了代码逻辑。
在金融交易系统等延迟敏感场景中,std::expected 的优势尤为明显——它消除了异常处理的堆栈展开开销,同时提供了明确的错误类型。在迁移策略上,建议从新 API 开始,逐步向核心路径推广。