引言
在程序设计的过程中,错误是常态,一个健壮的程序必须包含良好的错误处理。这要求我们首先理解错误的分类,并且掌握足够的工具。
一般地,错误可以分为可恢复的错误和不可恢复的错误:
可恢复的错误是指那些对程序运行不致命的错误,例如用户传递了错误的参数
相应地,不可恢复的错误是指对程序运行致命的错误,遇到这种错误,通常要清理资源后退出。
需要注意的是,上述两种分类所包含的具体错误类型并不是固定的,而是跟应用场景有关,例如,内存不足在某些情况下可能是一个可恢复的错误。
错误码
错误码也有多种形式:
返回值专门用作错误码,例如
bool
、int
和std::error_code
或自定义的类型返回值“无效”的部分用作错误码,例如
read
正常情况下应该返回一个正数,但是当遇到EOF
或者出错时分别返回0
和负数
异常
断言
断言用于检查内部逻辑必须满足的不变量(invariant),例如
// Private interface
//
// NOTE: Do NOT do this in real code, use loop instead.
static int fact_internal(int n, int acc) {
// C++ idiom (because assert accepts only 1 argument)
assert(n >= 0 && "n < 0");
if (n < 2)
return acc;
return fact_internal(n-1, acc*n);
}
// Public interface
int fact(int n) {
if (n < 0)
throw std::invalid_argument();
return fact_internal(n, 1);
}
异常的性能和 -fno-exceptions
尽管合法,但是异常的性能决定了它不适合作为控制流
默认的 libstdc++/libc++ 会使用异常,例如 new 如果分配的内存过大,会抛出 std::bad_alloc ,使用 -fno-exceptions 只是禁用了异常处理,无法禁止 libstdc++ 抛出异常[bib:-fno-exceptions],如果你想,可以自己用 -fno-exceptions 编译一个无异常的标准库。但是,像 new 这种情况可能会直接 std::abort 而不是还能挽救。 |
回调
void get_all(const vector<URL> &urls,
function<void(const Response &)> cb,
function<void(const ErrorInfo &)> err_cb) {
for (const auto &u : urls) {
Response resp = http_client.get(u.str());
}
}
实例:实现一个 Expected<T>
在一些函数式语言中,支持这样一种结构,用 C++ 表示大概是:
Result<int, ArithError> res = safe_add(left, right);
if (res) // success
std::cout << *res << '\n';
else
std::cerr << res.get_error() << '\n';
这种特性底层依赖一种称为“代数数据类型”的机制(有时也会称为“tagged union”),大概是这样:
struct Result {
enum Variant { OK, ERR } tag;
union {
T data;
E err;
} u;
};
但是,C++ 并没有天然支持这种结构,我们只能通过 struct
套 union
的方式来模拟,然而,这样存在内存管理的问题,比较麻烦。C++17 引入了 std::variant
解决了大部分的问题,但是还存在一个小问题,就是各个类型必须是不同的,也就是说,这样的代码:
std::variant<int, int> v;
无法通过编译。
个人理解,这应该是因为 variant 支持 std::get 获取数据,如果两个唯独相同,就没办法确认 std::get<int> 到底想要获取哪一个;另外,这样还存在理解上的问题。 |
这个问题某种程度上导致我们不能直接依赖 std::variant
来实现 Expect<T>
。
那咋办呢?有句名言,所有的问题都能通过增加中间层解决,在这里,我们可以创建一个中间层包装 E
:
class ErrorInfo {
public:
virtual ~ErrorInfo() = default;
};
用户在使用时,需要先令表示错误的类继承自 ErrorInfo
:
class ArithError : public ErrorInfo {
public:
// enum classes cannot inherits other classes.
enum class Type {
OVERFLOW,
DIV_BY_ZERO,
};
public:
static const ArithError E_OVERFLOW{Type::OVERFLOW};
static const ArithError E_DIV_BY_ZERO{Type::DIV_BY_ZERO};
private:
const Type typ_;
};
这样我们的接口就是 Expect<T>
而不是 Expect<T, E>
了。
参考
[bib:-fno-exceptions] https://stackoverflow.com/questions/6049563/with-fno-exceptions-what-happens-with-new-t