在 C++ 的世界里,错误处理一直是一个痛点。传统的做法要么是返回一个错误码(容易被忽视),要么是抛出异常(性能开销大,且难以追踪调用链)。C++23 引入的
<expected>库,提供了一种优雅且高效的解决方案:std::expected<T, E>。
在 C++23 中,std::expected<T, E> 已正式并入标准库(头文件 )。它是一种可区分联合体(discriminated union),用于存储一个成功的结果值(类型为 T)或一个代表失败的错误值(类型为 E)。
它是对自定义 Result 结构的官方标准化,被认为是现代 C++ 处理错误(代替异常和错误码引用)的最佳实践。
核心用意:强制将"业务数据"与"错误状态"绑定在一起,避免了传统 C++ 接口中"返回布尔值再用引用传出数据"或"返回错误码再抛异常"的臃肿写法。
优点:
- 统一返回路径:函数只有一个返回出口(Result),无论成功还是失败。
- 语义化判空:利用 explicit operator bool() 允许你像检查指针一样检查结果。
- 错误上下文:除了错误码(ec),还带有了人类可读的 message。
本文将深入解析 std::expected 的设计理念、实现原理,并提供一个简易的 C++17 兼容实现和用法示例。
1. 什么是 std::expected<T, E>?
std::expected<T, E> 是一个模板类,它是一个可区分联合体 (discriminated union),意味着它在内部可以持有两种状态之一:
- 成功状态 :包含一个类型为
T的有效值(例如:计算结果、读取到的配置)。 - 失败状态 :包含一个类型为
E的错误值(例如:错误码枚举、错误描述字符串)。
它强制调用者必须显式地检查结果,不可能"忽略"错误状态,从而大大提高了代码的安全性。
2. 简易实现(C++17 兼容)
如果无法立即使用 C++23 编译器,可以参考以下简易实现:
方案一:基于 std::variant 的实现(c++17推荐)
cpp
#include <variant>
#include <string>
#include <system_error>
#include <optional>
#include <stdexcept>
// 示例错误码
enum class ErrorCode {
Success = 0,
DeviceNotFound,
InvalidParameter
};
// 错误信息结构
struct ErrorInfo {
ErrorCode code{};
std::string message;
explicit operator bool() const noexcept {
return code != ErrorCode::Success;
}
};
// --------------------------------------------------------
// 基于 std::variant 的 Result 实现
// --------------------------------------------------------
template<typename T>
class Result {
public:
// 成功构造函数
Result(T val) : data_(std::move(val)) {}
// 失败构造函数
Result(ErrorInfo err) : data_(std::move(err)) {}
// 检查是否成功
bool has_value() const noexcept {
return std::holds_alternative<T>(data_);
}
explicit operator bool() const noexcept {
return has_value();
}
// 获取值(失败时抛出异常)
T& value() {
if (auto* val = std::get_if<T>(&data_)) {
return *val;
}
throw std::runtime_error("Accessing error value");
}
const T& value() const {
if (auto* val = std::get_if<T>(&data_)) {
return *val;
}
throw std::runtime_error("Accessing error value");
}
// 获取错误信息
ErrorInfo& error() {
if (auto* err = std::get_if<ErrorInfo>(&data_)) {
return *err;
}
throw std::runtime_error("Accessing value as error");
}
const ErrorInfo& error() const {
if (auto* err = std::get_if<ErrorInfo>(&data_)) {
return *err;
}
throw std::runtime_error("Accessing value as error");
}
// 安全访问值(失败时返回默认值)
T value_or(T default_value) const {
if (has_value()) {
return std::get<T>(data_);
}
return default_value;
}
// 解引用运算符
T& operator*() { return value(); }
const T& operator*() const { return value(); }
// 箭头运算符
T* operator->() { return &value(); }
const T* operator->() const { return &value(); }
private:
std::variant<T, ErrorInfo> data_;
};
// void 特化版本
template<>
class Result<void> {
public:
// 成功构造函数
Result() : has_error_(false) {}
// 失败构造函数
Result(ErrorInfo err) : error_(std::move(err)), has_error_(true) {}
bool has_value() const noexcept { return !has_error_; }
explicit operator bool() const noexcept { return !has_error_; }
ErrorInfo& error() {
if (!has_error_) {
throw std::runtime_error("No error present");
}
return error_;
}
const ErrorInfo& error() const {
if (!has_error_) {
throw std::runtime_error("No error present");
}
return error_;
}
private:
ErrorInfo error_;
bool has_error_;
};
// 便捷函数
template<typename T>
Result<T> Ok(T value) {
return Result<T>(std::move(value));
}
inline Result<void> Ok() {
return Result<void>();
}
template<typename T>
Result<T> Err(ErrorCode code, const std::string& message = "") {
return Result<T>(ErrorInfo{code, message});
}
inline Result<void> Err(ErrorCode code, const std::string& message = "") {
return Result<void>(ErrorInfo{code, message});
}
方案二:简单值+错误码封装
cpp
#include <system_error>
#include <string>
// 示例错误码
enum class ErrorCode {
Success = 0,
DeviceNotFound,
InvalidParameter
};
// 通用结果类型:简单值+错误码封装
template<typename T>
struct SimpleResult {
T value{}; // 成功结果
std::error_code ec{}; // 0 表示成功,非0 表示失败
std::string message; // 可选错误描述
bool has_value() const noexcept { return !ec; }
explicit operator bool() const noexcept { return !ec; }
// 安全获取值(失败时返回默认值)
T value_or(T default_value) const {
return has_value() ? value : default_value;
}
};
// void 特化
template<>
struct SimpleResult<void> {
std::error_code ec{};
std::string message;
bool has_value() const noexcept { return !ec; }
explicit operator bool() const noexcept { return !ec; }
};
// 将自定义 ErrorCode 映射到 std::error_code
inline std::error_code MakeErrorCode(ErrorCode code) {
return std::error_code(static_cast<int>(code), std::generic_category());
}
// 便捷函数
template<typename T>
SimpleResult<T> MakeResult(T value) {
SimpleResult<T> r;
r.value = std::move(value);
return r;
}
template<typename T>
SimpleResult<T> MakeError(ErrorCode code, const std::string& message = "") {
SimpleResult<T> r;
r.ec = MakeErrorCode(code);
r.message = message;
return r;
}
3. 用法示例
接下来我们看看如何使用这种模式:
A. 函数定义(返回 std::expected 或兼容类型)
cpp
// 使用 C++23 std::expected
#include <expected>
std::expected<double, ErrorCode> GetPosition(int axisId) {
if (axisId < 0 || axisId > 16) {
// 使用 std::unexpected 显式返回错误
return std::unexpected(ErrorCode::InvalidParameter);
}
// 成功时直接返回值
return 100.25;
}
// 使用我们的 Result 类型
Result<double> GetPositionCompat(int axisId) {
if (axisId < 0 || axisId > 16) {
return Err<double>(ErrorCode::InvalidParameter,
"ID out of range");
}
// 模拟调用
bool driverSuccess = true; // 假设成功
if (!driverSuccess) {
return Err<double>(ErrorCode::DeviceNotFound,
"Device not responding");
}
return Ok(100.25);
}
B. 返回值判断与使用
调用方必须处理两种可能的结果:
cpp
void ProcessData() {
// 方法一:使用 if (result) 判断(推荐)
auto result = GetPositionCompat(5);
if (result) {
double position = *result; // 使用解引用运算符获取值
std::cout << "Position is: " << position << std::endl;
// 或者使用 .value()
// double position = result.value();
} else {
auto err = result.error();
std::cerr << "Failed to get position. "
<< "Error Code: " << static_cast<int>(err.code)
<< ", Message: " << err.message << std::endl;
}
// 方法二:使用 value_or 提供默认值
double safePosition = result.value_or(0.0);
// 方法三:链式处理多个操作
ProcessMultiple();
}
void ProcessMultiple() {
// 模拟处理多个轴
for (int axisId : {1, 2, 5, 100}) {
auto result = GetPositionCompat(axisId);
if (!result) {
std::cout << "Axis " << axisId << " failed: "
<< result.error().message << std::endl;
continue;
}
std::cout << "Axis " << axisId << " position: "
<< *result << std::endl;
}
}
C. 复杂场景:组合多个操作
cpp
// 定义多个可能失败的操作
Result<double> GetVelocity(int axisId) {
if (axisId < 0 || axisId > 16) {
return Err<double>(ErrorCode::InvalidParameter);
}
return Ok(50.0); // 模拟返回值
}
Result<bool> IsEnabled(int axisId) {
if (axisId == 100) { // 模拟不存在的轴
return Err<bool>(ErrorCode::DeviceNotFound);
}
return Ok(true);
}
// 组合多个操作
Result<std::tuple<double, double, bool>> GetStatus(int axisId) {
auto posResult = GetPositionCompat(axisId);
if (!posResult) {
return Err<std::tuple<double, double, bool>>(
ErrorCode::InvalidParameter,
"Failed to get position: " + posResult.error().message
);
}
auto velResult = GetVelocity(axisId);
if (!velResult) {
return Err<std::tuple<double, double, bool>>(
ErrorCode::InvalidParameter,
"Failed to get velocity"
);
}
auto enabledResult = IsEnabled(axisId);
if (!enabledResult) {
return Err<std::tuple<double, double, bool>>(
ErrorCode::DeviceNotFound,
"Axis not enabled"
);
}
return Ok(std::make_tuple(*posResult, *velResult, *enabledResult));
}
4. 为什么它是现代 C++ 的首选?
- 强制处理 :编译器会鼓励你检查
has_value()。相较于传统错误码,它更难被意外忽略。 - 性能 :
std::expected通常是零开销抽象,与返回一个包含T和E的结构体性能一致。它避免了异常处理机制带来的堆栈展开开销。 - 语义清晰:函数签名明确表达了"预期成功,但可能失败"的意图。
- 函数式编程支持 (C++23 Monadic) :
std::expected支持链式操作.and_then()、.transform()、.or_else(),使得复杂的错误处理逻辑可以写得非常优雅。
5. 与异常和错误码的对比
| 特性 | 异常 (Exceptions) | 错误码 (Error Codes) | std::expected |
|---|---|---|---|
| 性能开销 | 高(堆栈展开) | 低 | 低(零开销抽象) |
| 强制处理 | 可选(可能被忽略) | 可选(经常被忽略) | 强制(必须检查) |
| 调用链追踪 | 困难 | 困难 | 容易(错误随返回值传递) |
| 代码清晰度 | 高(分离正常/异常路径) | 低(与正常逻辑混合) | 高(明确成功/失败) |
| 类型安全 | 高 | 低(可能用错错误码) | 高(类型安全联合) |
6. 最佳实践
- 为错误类型定义丰富的语义:使用枚举或结构体而非简单整数
- 提供有意义的错误信息:包含上下文信息,便于调试
- 尽早处理错误:不要将错误传递太远
- 使用 monadic 操作(C++23):简化错误处理逻辑
- 考虑性能影响:对于性能关键路径,评估错误处理开销
7. 总结
std::expected 代表了 C++ 错误处理的现代化方向,它结合了错误码的性能优势和异常的语义清晰度。虽然 C++23 才正式引入,但其设计理念可以追溯到函数式编程中的 Either 类型,并在 Rust 的 Result 类型中得到了验证。
对于尚未升级到 C++23 的项目,本文提供的兼容实现可以作为过渡方案。无论使用标准库实现还是自定义实现,采用这种模式都能显著提高代码的健壮性和可维护性。