C++ 标准库中的异常是标准库的一个组成部分, 但异常并不是 STL 的一部分, 所以下面这些内容里面不会展现任何关于泛型乃至模板相关可能令人不适的内容.
那, 先在 C 身上找点错误处理相关的乐子.
使用返回码进行错误处理
上篇文章聊的是一个非常具体的异常处理, 而在这个环境中, 使用异常是不得已的事情. 在 C 语言单调的世界里, 异常还是不存在的, 大家都非常和谐地使用特别的返回值来标记错误发生 (比如 fgetc 返回 EOF 表示文件读完了), 甚至返回值就直接是错误码本身 (如 fseek).
错误码机制会严重破坏结构化程序设计, 虽然 C 对自己的定位正是结构化语言中的战斗机. 特别是, 如果一段代码中充满如堆内存申请, 文件 IO 这些容易发生错误的调用, C 代码的错误处理方式便从骨子里透露出一股原始浑厚的蛮荒气息, 比如下面这段
int encrypt(char const* src_file, char const* dst_file, char const* key) { int ret = 0; void* buffer; FILE* dstf; FILE* srcf = fopen(src_file, "rb"); if (NULL == srcf) { return 1; } dstf = fopen(dst_file, "wb"); if (NULL == dst_file) { ret = 1; goto EXIT_CLOSE_SRC; } buffer = malloc(BLOCK_SIZE); if (NULL == buffer) { ret = 1; goto EXIT_CLOSE_DST; } while (0 == read_buffer(buffer, BLOCK_SIZE, srcf)) { encrypt_block(buffer, key); if (0 != write_buffer(buffer, BLOCK_SIZE, dstf)) { ret = 1; goto EXIT_FREE_BUFFER; } } if (!feof(srcf)) { ret = 1; } EXIT_FREE_BUFFER: free(buffer); EXIT_CLOSE_DST: fclose(dstf); EXIT_CLOSE_SRC: fclose(srcf); return ret; }
这里还只显示了这个函数的情况, 而它的调用者可能要应对更多, 比如将无法处理的错误逐个栈帧返回, 最终程序流程会显得极其难看. 不过在现代技术的帮助下, 我们有很多方法能避免上面这种结构混乱化程序设计, 比如换成像 C++ 这样支持异常的语言.
C++ 异常 抛与接
异常的优势在于, 它彻底分离错误出现与错误处理代码, 至于能不能增强代码健壮性那是另一回事, 也许程序会漏捕某些类型的异常, 当然这属于软件 bug 的范畴, 这里就不说了.
异常还有一个特性是, 函数会抛出什么, 在代码中会清晰的写出来 (而不是像错误码那样写在尘封的文档里), 不过这一点 C++ 比较扯, 即使一个函数声明什么也不抛 (throw()), 也可以随便抛. 这在编译时并不检查, 但是在生成的执行代码中会体现, 比如下面这段代码
#include <iostream>
#include <string>
void func_throw_int(int i) throw (std::string)
{
throw i;
}
int main()
{
try {
func_throw_int(10);
} catch (int) {
std::cout << "function throws integer." << std::endl;
} catch (std::string) {
std::cout << "function throws string." << std::endl;
} catch (...) {
std::cout << "exception caught." << std::endl;
}
return 0;
}
最终的结果是, 谁也没能真正捕获到这个在栈帧中穿梭的整数, 程序直接崩溃. 如下代码也会这样
#include <iostream>
#include <string>
void func_throw_int(int i) throw (std::string)
{
throw i;
}
void func_wrapper(int i) throw (int, std::string)
{
func_throw_int(i);
}
int main()
{
try {
func_wrapper(10);
} catch (int) {
std::cout << "function throws int." << std::endl;
} catch (std::string) {
std::cout << "function throws string." << std::endl;
} catch (...) {
std::cout << "exception caught." << std::endl;
}
return 0;
}
可以看出, C++ 编译器还是会根据函数的异常描述选择性地在栈帧上布下眼线, 而不是粗放式地乱抓一气, 即使是 catch (...) 也不是银弹, 大家还是养成良好习惯, 不要乱声明乱抛乱抓了哦.
这部分的最后再介绍一个几乎不为人知的标准库函数, (我也不太清楚这个大隐隐于标准库的家伙到底是为什么被设计出来的)
typedef void (std::* unexpected_handler)();
std::unexpected_handler std::set_unexpected(std::unexpected_handler new_handler);
它接受一个函数指针作为参数, 返回之前值班的函数. 传入的这个函数执行的时机是, 当有某个出问题的函数中抛出的异常类型与函数实际声明的异常不一致, 这个函数的栈帧被弹出后的那个时刻. 另一方面, 由于 std::unexpected_handler 型函数不接受任何参数, 从而也就无法诊断任何跟误抛相关的信息, 还不如上篇文章谈到的简易 signal 呢. 下面这段例子演示它如何工作
#include <iostream>
#include <string>
#include <exception>
void unexpected_throw()
{
std::cout << "d'oh!" << std::endl;
}
void func_throw_int(int i) throw(std::string)
{
throw i;
}
int main()
{
std::set_unexpected(unexpected_throw);
func_throw_int(10);
return 0;
}
效率
选择 C++ 开发也许看中的就是 C++ 程序运行时高效, 所以错误处理的效率很自然地会在某个时刻 (很可能是解决掉内存分配器的效率问题之后 :) 被提上议程. 异常的效率很难衡量, 特别是如果需要与 C 的错误码机制的效率相提并论时, 两者几乎没什么可比性.
如果使用异常来处理错误, 一段平铺直叙的代码在没有发生错误的情况下会表现得非常好, 但是一旦发生错误, 异常处理程序会产生极大的开销; 而使用错误码加分支的方法, 在任何情况下表现都很平均, 而且分支预测成功率会显著地反映在错误处理代码中.
这里就不扯什么 "一般情况推荐用异常, 极端情况下使用错误码来提高平均效率" 之类的废话了, 总之, 这是个很有争议的问题, 纸上谈兵也没用, 这里就打住吧.