CppCoreGuidelines E.27 如果无法抛出异常,请有系统的使用错误码
13 August 2023
“If you can’t throw exceptions, use error codes systematically”
理由
比较系统的应用出错处理策略,可以减小忘记处理错误的风险。
请参考:模拟 RAII
注意
这里有几个问题需要澄清:
- 你打算怎么把错误标记传递出函数?
- 你打算怎么在函数退出的时候释放所有资源?
- 你打算怎么使用错误标记?
一般来说,返回错误标记意味着返回两个值:函数结果和错误标记。错误标记可以是对象的一部分,比如某个对象有一个 valid()
函数,用来标记错误。或者返回成对的一对数值。
例子
Gadget make_gadget(int n) { // ... } void user() { Gadget g = make_gadget(17); if (!g.valid()) { // error handling } // ... }
这个方法属于模拟 RAII 资源管理。 valid()
函数可以返回一个错误标记。错误标记可以是枚举类型的值。
例子
那么,如果我们不能或者不想要修改 Gadget
类型呢?这种情况,我们只能返回一对数值。
比如:
std::pair<Gadget, error_indicator> make_gadget(int n) { // ... } void user() { auto r = make_gadget(17); if (!r.second) { // error handling } Gadget& g = r.first; // ... }
正如代码所展示的, std::pair
可以作为返回类型。有些人更喜欢自定义类型。比如:
Gval make_gadget(int n) { // ... } void user() { auto r = make_gadget(17); if (!r.err) { // error handling } Gadget& g = r.val; // ... }
我们选择返回自定义类型是因为自定义类型可以给成员命名。这样就避免使用
std::pair
出现的混淆。像 first
second
之类的名字太晦涩。
例子
一般来说,你必须因为错误而退出之前进行清理工作。可能会看起来很杂乱:
std::pair<int, error_indicator> user() { Gadget g1 = make_gadget(17); if (!g1.valid()) { return {0, g1_error}; } Gadget g2 = make_gadget(31); if (!g2.valid()) { cleanup(g1); return {0, g2_error}; } // ... if (all_foobar(g1, g2)) { cleanup(g2); cleanup(g1); return {0, foobar_error}; } // ... cleanup(g2); cleanup(g1); return {res, 0}; }
如果函数中有多个资源,多个出错点的时候,模拟 RAII 的工作量不小。一个常见的技术是把清理动作收集在一起放到函数后面,从而可以避免重复(注意,
g2
的范围限定是不必要的,但是为了使得 goto
正确工作,需要做这个范围限定。
std::pair<int, error_indicator> user() { error_indicator err = 0; int res = 0; Gadget g1 = make_gadget(17); if (!g1.valid()) { err = g1_error; goto g1_exit; } { Gadget g2 = make_gadget(31); if (!g2.valid()) { err = g2_error; goto g2_exit; } if (all_foobar(g1, g2)) { err = foobar_error; goto g2_exit; } // ... g2_exit: if (g2.valid()) cleanup(g2); } g1_exit: if (g1.valid()) cleanup(g1); return {res, err}; }
函数越长,奇技淫巧越多。 finally
可以减轻一些痛苦。另外,程序越大,越难系统地实施这种基于错误标记的错误处理策略。
请查看:返回多个值