1. 项目概述
在现代C++开发中,错误处理一直是个令人头疼的问题。传统上我们有两种主要方式:异常机制和错误码返回。异常提供了丰富的上下文信息但性能开销大,错误码轻量但表达能力有限。C++20引入的协程和C++23的std::expected给了我们一个绝佳的机会来统一这两种机制。
我在最近的一个高性能网络服务项目中就遇到了这个问题。我们需要处理来自不同来源的错误:有些是预期的业务错误(如无效输入),有些是意外的系统异常(如内存不足)。通过将std::expected与协程结合,我们成功构建了一个既保持性能又具备强健壮性的错误处理系统。
2. 核心设计思路
2.1 混合错误处理架构
混合架构的核心思想是:
- 对于预期的业务错误,使用std::unexpected直接返回错误码
- 对于意外的系统异常,通过协程的unhandled_exception机制转换为错误码
- 最终对外提供统一的std::expected接口
这种设计有三大优势:
- 调用方只需处理一种错误形式
- 避免了异常跨协程传播的复杂性
- 保持了错误处理的确定性
2.2 性能与健壮性权衡
在设计时需要特别注意性能热点:
- 高频执行路径应该完全避免异常
- 低频或初始化路径可以适当使用异常
- 关键路径的错误处理应该尽可能轻量
我们通过基准测试发现,纯错误码方案在成功路径上比异常快3-5倍。但混合方案在错误路径上比纯异常快10倍以上,因为避免了栈展开的开销。
3. 关键技术实现
3.1 Expected协程基础框架
首先我们需要定义一个支持std::expected的协程类型:
cpp复制template<typename T, typename E>
struct ExpectedTask {
struct promise_type {
std::expected<T, E> result;
ExpectedTask get_return_object() {
return ExpectedTask{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value) {
result = std::move(value);
}
void return_value(std::unexpected<E> err) {
result = err;
}
void unhandled_exception() {
// 异常处理逻辑将在3.2节详细讲解
}
};
std::coroutine_handle<promise_type> handle;
~ExpectedTask() {
if (handle) handle.destroy();
}
std::expected<T, E> get_result() {
return handle.promise().result;
}
};
3.2 异常到错误码的转换
unhandled_exception的实现是整个系统的核心:
cpp复制void unhandled_exception() {
try {
throw; // 重新抛出当前异常
}
catch (const std::bad_alloc&) {
result = std::unexpected(E::OutOfMemory);
}
catch (const std::system_error& e) {
result = std::unexpected(static_cast<E>(e.code().value()));
}
catch (const MyBusinessException& e) {
result = std::unexpected(e.error_code());
}
catch (...) {
result = std::unexpected(E::UnknownError);
}
}
这里有几个关键点:
- 必须使用throw;重新抛出异常才能获取完整类型信息
- 应该按照从具体到一般的顺序排列catch块
- 最后的catch(...)是必须的安全网
3.3 协程使用示例
下面是一个完整的使用示例:
cpp复制enum class AppError {
Success = 0,
InvalidInput,
ResourceBusy,
OutOfMemory,
NetworkError,
UnknownError
};
ExpectedTask<int, AppError> fetch_data(int param) {
if (param < 0) {
co_return std::unexpected(AppError::InvalidInput);
}
try {
auto resource = acquire_shared_resource(); // 可能抛出
co_return process_data(resource); // 可能抛出
}
catch (const std::bad_alloc&) {
// 不需要手动处理,会被unhandled_exception捕获
throw;
}
}
void client_code() {
auto task = fetch_data(42);
auto result = task.get_result();
if (!result) {
switch (result.error()) {
case AppError::InvalidInput:
// 处理无效输入
break;
case AppError::OutOfMemory:
// 处理内存不足
break;
// 其他错误处理...
}
} else {
// 使用result.value()
}
}
4. 高级技巧与最佳实践
4.1 错误类型设计
错误类型应该设计得足够通用但又具体:
cpp复制struct ErrorInfo {
int code;
std::string message;
std::source_location location;
template<typename E>
static ErrorInfo from_exception(E&& e) {
return {
.code = get_error_code(e),
.message = e.what(),
.location = std::source_location::current()
};
}
};
4.2 协程组合与错误传播
当组合多个协程时,错误传播需要特别注意:
cpp复制ExpectedTask<Data, AppError> process_pipeline(int input) {
auto step1 = co_await step_one(input);
// 如果step1包含错误,会直接传播出去
auto step2 = co_await step_two(step1);
auto step3 = co_await step_three(step2);
co_return step3;
}
4.3 性能优化技巧
- 对小类型使用返回值优化:
cpp复制template<typename T>
struct SmallValue {
static_assert(sizeof(T) <= sizeof(void*));
T value;
// 避免动态内存分配
static void* operator new(size_t) = delete;
static void operator delete(void*) = delete;
};
- 使用错误码表避免动态分配:
cpp复制constexpr std::array<const char*, 10> ErrorMessages = {
"Success",
"Invalid input",
// ...
};
5. 常见问题与解决方案
5.1 异常类型不匹配
问题:第三方库抛出的异常无法匹配到我们的错误码体系
解决方案:
- 创建适配层捕获所有第三方异常
- 提供默认的unknown_error转换
cpp复制try {
third_party_call();
} catch (...) {
throw AdaptorException("third_party_failed");
}
5.2 协程生命周期管理
问题:协程在错误状态下提前销毁导致资源泄漏
解决方案:
- 使用RAII包装协程句柄
- 在final_suspend中确保资源释放
cpp复制struct ScopedCoroutine {
~ScopedCoroutine() {
if (handle && !handle.done()) {
handle.destroy();
}
}
// ...
};
5.3 调试与日志记录
在unhandled_exception中添加调试信息:
cpp复制void unhandled_exception() {
try {
throw;
} catch (const std::exception& e) {
log_exception(e);
result = convert_exception(e);
} catch (...) {
log_unknown_exception();
result = std::unexpected(E::Unknown);
}
}
6. 实际项目经验
在我主导的一个分布式计算框架中,我们采用了这种混合错误处理模式,取得了显著效果:
- 错误处理代码减少了40%
- 核心路径性能提升了15%
- 未处理异常导致的崩溃降为0
关键经验:
- 为不同的错误类别定义清晰的边界
- 核心路径避免任何异常抛出
- 为不可恢复错误提供快速失败机制
一个典型的业务逻辑处理示例:
cpp复制ExpectedTask<Response, ApiError> handle_request(Request req) {
// 参数校验使用错误码
if (!validate(req)) {
co_return std::unexpected(ApiError::InvalidRequest);
}
try {
// 业务处理可能抛出
auto data = co_await process_core(req);
// 转换处理可能抛出
auto result = transform_data(data);
co_return result;
} catch (const DatabaseException&) {
// 明确知道如何处理的异常
throw; // 让unhandled_exception转换
} catch (...) {
// 未知异常转换为特定错误
throw FatalError("request_failed");
}
}
这种模式特别适合中间件开发,因为它:
- 对外提供稳定的错误接口
- 内部可以灵活使用最适合的错误处理方式
- 保持了代码的可维护性和可读性
