PAYATHON 2026

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]] 是合理选择;如果它属于一个需要整体成功、失败后交由外层终止或恢复的操作,异常也很自然。接口文档应明确失败如何报告,以及失败后对象保留什么状态。

备注:内容仅供参考。