C++ 中何时应该抛出异常,何时应该返回错误码?
如果调用方经常需要处理非法输入,返回明确的错误类型通常更方便;如果函数无法完成约定的操作,需要将错误交给更上层统一处理,异常通常更合适。还要遵循所在项目或库的错误处理约定。
对于这个 write() 示例,两种设计都可以。仅凭“异常不会被忽略”,还不足以确定应该采用异常。
未捕获异常会终止程序,不等于处理了错误
返回错误码时,如果调用方没有检查,程序可能继续执行,错误地认为写入已经成功。接口设计需要考虑这种情况。
如果改为抛出异常,失败会打断正常控制流,并向外寻找匹配的异常处理器。始终找不到处理器时,包括异常传播出 main() 的情况,就会调用 std::terminate(),默认行为会导致程序异常终止。
异常可以通过控制流向外传播失败,但调用方仍需在合适的位置处理它,才能恢复、重试或提示用户。程序也可能用空的 catch 块吞掉异常,因此两种方式都依赖合理的使用约定。
根据调用方如何处理失败来选择
| 场景 | 通常适合的方式 | 原因 |
|---|---|---|
| 用户输入可能不合法,调用方需要提示并重新输入 | 返回错误类型 | 失败是当前业务流程需要立即判断的结果 |
| 中间多层函数无法处理失败,只能把错误传给上层 | 异常 | 可以跨越这些调用层,集中处理 |
| 接口必须兼容 C,或项目明确禁用异常 | 错误码 | 需要遵守接口和构建约束 |
| 构造函数无法建立有效对象 | 异常,或使用返回结果的工厂函数 | 构造函数没有普通返回值可用于报告失败 |
“是否常见”只是判断因素之一。还要考虑调用方是否需要就地处理、失败后能否继续当前操作,以及错误应在哪一层处理。
例如,同样是非法图片内容,编辑器可能希望返回校验结果并提示用户;批处理操作也可能选择抛出异常,在任务边界记录失败并停止当前任务。具体取决于接口的使用方式。
返回错误码时,使用明确类型并提示调用方检查
原示例使用 int 返回结果,调用方容易忽略它,也容易将它与其他整数混用。可以改用 enum class,并使用 C++17 的 [[nodiscard]]:
#include <iostream>
#include <utility>
#include <vector>
enum class WriteResult {
success,
invalid_input
};
class ImageFile {
public:
[[nodiscard]] WriteResult write(std::vector<char> v) {
for (char ch : v) {
if (ch != 'a' && ch != 'b' && ch != 'c') {
return WriteResult::invalid_input;
}
}
contents_ = std::move(v);
return WriteResult::success;
}
private:
std::vector<char> contents_;
};
int main() {
ImageFile image;
std::vector<char> v{'a', 'b', 'c', 'x'};
const auto result = image.write(v);
if (result == WriteResult::invalid_input) {
std::cerr << "Invalid image content\n";
return 1;
}
return 0;
}
这个例子省略了与错误处理无关的 File 基类和文件名构造逻辑。非法输入会在修改 contents_ 之前被拒绝。
[[nodiscard]] 鼓励编译器在返回值被丢弃时给出诊断,通常表现为警告,但不能强制调用方正确处理错误。是否将相关警告视为构建失败,由项目配置决定。
接口还需要明确覆盖哪些失败:这里的返回值只报告内容校验结果,参数复制等涉及内存分配的操作仍可能抛出异常。返回错误码的函数也可能抛出异常。
使用异常时,在能够作出决定的位置捕获
如果接口约定非法内容属于异常,可以把上例的 write() 替换为以下实现,并添加 <stdexcept>:
void write(std::vector<char> v) {
for (char ch : v) {
if (ch != 'a' && ch != 'b' && ch != 'c') {
throw std::invalid_argument(
"Image content must contain only a, b, or c"
);
}
}
contents_ = std::move(v);
}
调用方可以在能够提示用户、结束任务或选择其他输入的位置处理异常:
try {
image.write(v);
} catch (const std::invalid_argument& error) {
std::cerr << error.what() << '\n';
// Report the failure or request different input.
}
如果中间函数无法处理这个错误,可以让异常继续传播,无需为了逐层转发而添加 catch。在需要恢复、补充上下文或执行其他明确动作时再捕获即可。
使用异常时也要做好资源管理。采用 RAII,让对象管理文件、锁和内存;异常被捕获并发生栈展开时,这些对象的析构函数可以完成清理。对于始终未被捕获的异常,不应依赖程序终止前一定会完成栈展开。
原示例中还有几个代码细节
'99'是多字符字面量,不表示一个值为 99 的 char,也不是字符串。用于这里的vector<char>列表初始化时可能触发窄化转换错误。演示非法输入时,使用'x'更清楚。- 参数
const vector<char> v是按值传递。由于 v 带有 const,move(v)通常无法使用需要非 const 右值的 vector 移动赋值,最终会发生复制。如果希望接收参数后移动到成员中,可以像示例一样使用std::vector<char> v。 INVAID_INPUT看起来是INVALID_INPUT的拼写错误。伪代码中也有缺失分号等问题,需要在实际实现时修正。
对于当前问题,如果 write() 主要供调用方反复校验输入,类型化返回值加 [[nodiscard]] 是合理选择;如果它属于一个需要整体成功、失败后交由外层终止或恢复的操作,异常也很自然。接口文档应明确失败如何报告,以及失败后对象保留什么状态。
备注:内容仅供参考。
