近期,知乎技术社区一则关于“C++异常很难用”的讨论帖引发广泛关注。发帖者援引多家一线互联网公司内部代码规范,称不少团队明确禁用异常机制,转而采用返回值、错误码或std::optional等替代方案。这一话题迅速燃起争议,参与讨论的开发者横跨游戏引擎、嵌入式系统、金融交易等不同领域,观点分歧显著。

异常机制:理论上的优雅与现实中的代价

C++标准异常(exception)自1998年首次引入,设计初衷是实现“错误处理与正常逻辑分离”。理想状态下,开发者只需在可能出错的函数后抛出异常,上层调用方通过catch块统一捕获,代码可读性得以提升。然而,现实中的C++异常处理面临三重痛点:

第一,性能开销不可控。异常抛出与栈展开(stack unwinding)涉及大量运行时操作,包括对象析构、内存管理、异常表查找等。在高频交易、实时渲染等场景下,微秒级延迟即可导致系统崩溃。有开发者实测,在循环中频繁触发异常,性能可下降数十倍。

第二,资源管理复杂化。在C++98/03时代,异常安全(exception safety)的三大保证(基本、强、不抛)成为许多团队的噩梦。即便现代C++引入RAII和智能指针,仍难以完全杜绝内存泄漏。一旦构造函数抛出异常,资源释放逻辑可能被跳过。

第三,心智负担重。异常会打乱控制流,使代码行为难以预测。某游戏引擎开发者坦言:“当你在堆栈深处看到20个可能抛出的点,却无法确定谁会捕获时,调试直接变为噩梦。”此外,部分编译器对异常支持不完善,导致可执行文件体积膨胀。

社区实践:三种主要替代方案

面对异常机制的争议,开发者社区逐渐形成三种主流实践:

方案一:返回错误码或bool值。 传统C风格,简单明了。缺点在于错误扩散——每层调用都需检查返回值,代码充斥着if-else,可读性下降。Google在其C++代码规范中明确禁止使用异常,转而采用gsl::final_action或absl::Status。

方案二:std::optional或std::expected(C++23)。 这类“可空值”类型让函数通过返回类型同时携带结果或错误信息。C++23引入的std::expected将错误类型纳入类型系统,更接近Rust的Result模式。然而,团队需统一错误枚举,否则极易退化回bool。

方案三:异常结合RAII的“保守主义”。 不少资深C++程序员坚持使用异常,前提是严格遵循“资源获取即初始化”和“仅对真实错误抛异常(非控制流)”。如Boost库的异常体系、Qt的信号槽机制均尝试优化异常使用体验。一个典型规则是:构造函数不应抛出异常;析构函数内禁止抛出。

专家视角:没有银弹,只有场景适配

C++标准委员会成员、微软工程师Herb Sutter曾多次撰文指出,异常并非“坏设计”,而是被错误用于控制流。他认为,开发者应区分“可预期错误”(如文件不存在,适合用optional)与“不可预期错误”(如内存耗尽,适合抛异常)。

国内C++社区知名博主“左耳朵耗子”(陈皓,已故)生前也曾强调:“异常是C++面向对象设计的重要工具,禁用异常意味着放弃STL中大量异常安全的容器操作。”然而,他同时承认,国内二线公司项目因程序员水平参差,禁用异常反而能降低维护成本。

行业趋势:新标准努力弥合裂痕

C++23标准对错误处理做出了最大变革:std::expected进入标准库,并新增了std::outcome(提案中)。这些特性借鉴了Rust、Swift等语言的经验,试图在“显式错误”与“异常”之间架桥。与此同时,Facebook(现Meta)开源的folly库、Google的Abseil都提供了比标准更易用的错误类型。

结语:问题不在异常,在人

回顾这场关于C++异常处理的持久争论,其本质是“自动 vs 手动”、“抽象 vs 透明”的哲学较量。对于刚入门C++的开发者而言,从异常安全基本法则学起,而非急于站队,或许更为务实。正如某位知乎高赞回答所言:“当你开始问‘异常怎么用’时,说明你根本没理解它为何存在。先写出清爽的错误码,再反思是否真的需要异常。”这条道路本身,就是一场深刻的程序设计思维训练。