现代 C++ 错误处理知多少(未完工)
配套视频:BV1QpWSekEJY
错误的分类
假设一个函数 open
的功能是打开文件。
int open(const char *path) {
if (!file_exists(path)) {
// 如果找不到文件怎么办?
}
// 成功找到文件:
return get_handle(path);
}
int main() {
int file = open("file.txt");
char buf[64];
read(file, buf, sizeof buf);
...
}
理想情况下,所有的函数都能成功执行,都能正常返回结果……
可现实中,我们不能假设一个程序,永远正确执行(例如文件可能被用户误删除,或者内存不够用等)。
更有甚者,有时错误是计划的一部分(例如文件不存在,则创建一个新文件,而不是将其视为不可修复的错误)。
特别是涉及 IO 操作的任务,出现一些细小错误的情况是很多的。要区分哪些是可以修复的错误,哪些是不可挽回的错误。
例如当网络连接失败时,我们可以重新尝试连接两三次,如果还是不行,那才认为是真的失败了。
因此,我们把错误分为两大类:
- 可恢复错误:不是特别严重的,甚至是计划之中的,经常发生的错误。可以通过一定操作来弥补这类错误,或将其转化为其他不同类型的错误。
- 不可恢复错误:非常严重的错误,或者是发生概率很低平时没必要特殊处理的错误。一旦发生,整个程序都无法继续执行下去,必须全身而退,整个进程或线程都将终止。
不可恢复错误
不可恢复错误的处理最简单,我们只需要在被调用者检测到错误的分支中,直接调用 exit
函数“终止程序”即可。
int open(const char *path) {
if (!file_exists(path)) {
// 找不到文件我就自杀!
exit(1);
// 程序不会执行到此
}
return get_handle(path);
}
int main() {
int file = open("file.txt");
char buf[64];
read(file, buf, sizeof buf);
...
}
- 缺点:
exit
会直接退出整个进程!没有任何给调用者挽回的机会,因此只能用于“不可恢复错误”这个类型。 - 优点:调用者无需做任何判断处理,写起来就好像被调用函数“总是成功”一样,总能返回结果。因为如果被调用者失败的话,他会调用
exit
自杀,就不会返回到调用者中了。
小时候看这集变成“码码的萤火虫”了。
可恢复错误
有时候,我们对于部分错误,是有挽回机会的,不希望因为一点可以修复的小错误就把整个程序终止掉。
要不要挽回应该由调用者的具体业务决定,而封装良好的 API(open
)应该忠实地把错误报告给调用者(main
)。
让调用者来决定要杀了还是抢救,而不是自作主张地直接自杀。
int open(const char *path) {
if (!file_exists(path)) {
// 找不到文件,就返回 -1 这个“出错特殊值”代替
return -1;
}
return get_handle(path);
}
int main() {
int file = open("file.txt");
if (file == -1) { // 缺点是 main 里面必须判断返回值是否为“出错特殊值”
// 如果找不到文件,尝试进行处理
create_empty_file("file.txt");
// 重新尝试打开
file = open("file.txt");
if (file == -1) { // 如果还是出错,那就没救了
exit(-1); // 直接自杀
}
}
char buf[64];
read(file, buf, sizeof buf);
...
}
我该如何抉择
调用者与被调用者
main
是调用者,open
是被调用者。
被调用者函数可能产生错误,也可能正常执行。
提前返回是好习惯!
异常
错误码
std::error_code
std::expected
也可以 boost::expected
替代。