使用错误代码对象进行C++错误处理
原文发表于codeproject,由本人翻译整理分享于此。
前言
我已经使用了本文描述的代码和机制近20年了,到目前为止,我还没有找到更好的方法来处理大型c++项目中的错误。最初的想法是从一篇文章(dr dobbs journal 2000年)中摘录出来的。我已经添加了一些新内容进去,使它更容易在生产环境中使用。
写这篇文章的冲动是最近发表在andrzej的c++博客。正如我们在本文后面将看到的那样,使用错误代码对象可以产生更清晰、更易于维护的代码。
背景
每个c++程序员都知道处理异常情况的传统方法有两种:第一种是从良好的旧c风格继承而来,返回错误代码,并希望调用者进行判断并采取适当的操作;第二种方法是抛出异常,并希望周围代码块捕获并处理该异常。c++ faq强烈支持第二种方法,认为它会使得代码更安全。
然而,使用异常也有其自身的缺点。代码变得更加复杂,用户必须知道所有可能引发的异常。这就是为什么旧的c++规范在函数声明中添加了“异常规范”。此外,异常会降低代码的效率。
错误代码对象被设计成类似于传统c错误代码的函数返回。最大的区别是,如果不进行判断,它们就会抛出异常。
让我们举个小例子,看看不同的实现会是什么样的。
首先,采用传统错误码的经典c方法:
int my_sqrt (float& value) { if (value < 0) return -1; value = sqrt(value); return 0; } int main () { double val = -1; // 注意,这里已经进行了返回值得检查 if (my_sqrt (val) == -1) printf ("square root of negative number"); // 有些人会忘记返回值检查 my_sqrt (val); // 这时候断言出错,因为我们没有检查返回值 assert (val >= 0); }
如果不检查结果,所有的坏事情都会发生,我们必须准备好使用所有传统的调试工具来找出问题。
使用传统c++异常,相同的代码可能如下所示:
void my_sqrt (float& value) { if (value < 0) throw std::exception (); value = sqrt(value); } int main () { double val = -1; // 注意,这里已经捕获异常 try { my_sqrt (val); } catch (std::exception& x) { printf ("square root of negative number"); } // 有些人可能会忘记捕获异常 my_sqrt (val); // 这时候断言出错,因为我们没有捕获异常 assert (val >= 0); }
异常处理在这样一个小例子中非常有用,因为我们可以看到my_sqrt函数使用try...catch包裹。但是,如果函数被深埋在库中,你可能不知道它可能抛出哪些异常。请注意,从my_sqrt函数签名中根本不知道它会抛出什么异常(如果它有抛出异常的话)。
现在.……咳咳..……错误代码对象(erc)登场:
erc my_sqrt (float& value) { if (value < 0) return -1; value = sqrt(value); return 0; } int main () { double val = -1; // 注意,这里进行返回值检查 if (my_sqrt (val) == -1) // (1) printf ("square root of negative number"); // 如果你喜欢异常处理,也是可以的 try { my_sqrt (val); } catch (erc& x) { printf ("square root of negative number"); } // 有些人可能忘记检查返回值 my_sqrt (val); // (2) // 程序会崩溃,因为有一个未捕获的异常 assert (val >= 0); }
在深入了解这种方法的魔力之前,请先观察几点:
- 首先,一个术语问题:为了区分传统的“c”错误代码和我的错误代码对象,在本文的其余部分,我将把“错误代码”称为我的错误代码对象。当我需要引用传统的“c”错误代码时,我将它们称为“c错误代码”。
- my_sqrt函数签名清楚地指示它将返回错误代码。在c++异常情况下,没有迹象表明它会抛出异常。很久以前,c++98有这些异常规范,但在c++11中就被废弃了。你可以在雷蒙德·陈(raymond chen)的文章中找到更多关于这一点的讨论(the sad history of the c++ throw(…) exception) specifier。c错误代码方案也没有明确返回的整数值是错误代码。
初窥error code对象
我们先来一个全貌展示,暂时忽略一些细节,后续再细讲。
当创建一个erc对象时,它有一个整数值(就像c错误代码)和一个活动标志。
class erc { public: erc (int val) : value (val), active (true) {}; //... private: int value; // 一个整数值 bool active; // 一个活动标志 }
如果释放erc对象时,活动标志被设置,则析构函数将会引发异常。
class erc { public: erc (int val) : value (val), active (true) {} // 析构函数检查活动标志,决定是否抛出异常 ~erc () noexcept(false) {if (active) throw *this;} //... private: int value; bool active; }
到目前为止,仍然没有什么特别之处:这仅仅是一个在析构函数中抛出异常的对象。也因为如此,我们必须使用noexcept(false)来修饰析构函数。
整数转换运算符则返回erc对象的整数值,并重置活动标志:
class erc { public: erc (int val) : value (val), active (true) {} ~erc () noexcept(false) {if (active) throw *this;} // 整数转换运算符,返回整数值,重置活动标志 operator int () {active = false; return value;} //... private: int value; bool active; }
由于活动标志已被重置,当erc对象超出作用域时,析构函数将不再抛出异常。通常,当对错误代码进行检查时,将调用整数转换运算符。
回顾一下前面简单的用法示例,在标记为(1)的注释算处,函数my_sqrt返回的erc对象与整数值进行比较,从而调用整数转换运算符。因此,活动标志将被重置,并且析构函数不会抛出异常。在标记为(2)的注释处,函数my_sqrt返回的erc对象,由于设置了活动标志,析构函数将引发异常。
遵循公认的unix惯例,正如亚里士多德所说,成功的方法只有一种,那就是数值‘0’表示成功。erc对象的数值为0则不抛出异常。任何其他数值都表示失败,并抛出异常(如果没有检查返回值)。
这是错误代码对象的整个概念的精髓,如dobbs journal的文章所示。然而,我无法抗拒接受一个简单的想法并使它变得更复杂的诱惑;继续阅读!
更多细节
前面只是全貌展示,忽略了一些细节。这些细节使错误代码功能更完善,便于把它集成到大型项目中。首先,我们需要一个移动构造函数和一个移动赋值操作符。目的是把活动标志传递给新对象,并使原对象的活动标志失效,确保只有一个活动的erc对象。
为了便于处理,我们还需要将错误代码分类的组件,这个组件是通过error facility对象(errfac)实现。除了数值和活动标志属性之外,erc还具有一个facility对象和一个严重性级别。erc析构函数并不像我们前面那样直接抛出异常,而是调用errfac::raise函数,与facility对象关联起来。在这个raise函数中,比较erc对象的严重性级别和facility对象关联的日志级别。如果erc对象的级别高于facility对象的日志级别,则errfac::raise()函数调用errfac::log()函数生成错误信息并抛出异常,或在超过预设级别时只记录错误信息。严重性级别是从unix syslog函数借用的:
名字 | 数值 | 动作 |
---|---|---|
error_pri_success | 0 | 总是不记录,不抛出 |
error_pri_info | 1 | 默认不记录,不抛出 |
error_pri_notice | 2 | 默认不记录,不抛出 |
error_pri_warning | 3 | 默认记录,不抛出 |
error_pri_error | 4 | 默认记录,抛出 |
error_pri_critical | 5 | 默认记录,抛出 |
error_pri_alert | 6 | 默认记录,抛出 |
error_pri_emerg | 7 | 总是记录,抛出 |
默认情况下,错误代码与默认的facility对象关联。但是,我们也可以定义不同的facility类,重新处理错误。例如,您可以为所有套接字错误定义一个专门的错误处理facility类,该类把错误代码转换为有意义的消息。具有不同的错误级别有利于测试或调试,通过改变某一类错误的抛出或日志记录级别。
一个更实用的例子
这篇博客文章前面提到的,一个http客户端程序的基本流程:
status get_data_from_server(hostname host) { open_socket(); if (failed) return failure(); resolve_host(); if (failed) return failure(); connect(); if (failed) return failure(); send_data(); if (failed) return failure(); receive_data(); if (failed) return failure(); close_socket(); // 有资源漏的可能 return success(); }
这里有个问题是,因为套接字没有关闭函数就返回,会产生资源泄漏。在这种情况下,让我们看看如何使用错误代码(指作者写的erc)。
如果我们想使用异常,代码可以如下所示:
// 函数声明,返回值得使用erc erc open_socket (); erc resolve_host (); erc connect (); erc send_data (); erc receive_data (); erc close_socket (); erc get_data_from_server(hostname host) { erc result; try { // 这些函数调用失败,会触发异常 open_socket (); resolve_host (); connect (); send_data (); receive_data (); } catch (erc& x) { result = x; // 返回erc对象给外部调用者 } close_socket (); // 清理 return result; }
毫无例外,相同的代码可以写成:
// 函数声明,返回值使用erc erc open_socket (); erc resolve_host (); erc connect (); erc send_data (); erc receive_data (); erc close_socket (); erc get_data_from_server(hostname host) { erc result; (result = open_socket ()) || (result = resolve_host ()) || (result = connect ()) || (result = send_data ()) || (result = receive_data ()); close_socket (); // 清理 result.reactivate (); return result; }
在上面的片段中,result已转换为整数,因为它必须参与逻辑或表达式。此转换重置活动标志,因此我们必须再次显式打开它,方法是调用reactivate()功能。如果所有函数调用都是成功的,那么结果就是0,而且,按照惯例它不会抛出异常。
最后
附件的源代码是高质量的、经过合理优化的,希望它不会更很难使用。演示项目是对流行的sqlite数据库的c++包装器。演示项目比较大,因为它包含了sqlite最新版本的代码(截至本文编写时,2019年11月)。源代码和演示项目都包括 doxygen文档。
历史
2019年11月12日:初版
源码和演示项目
download source code - 6.9 kb
download demo project - 2.2 mb
欢迎关注我的公众号【林哥哥的编程札记】,也欢迎赞赏,谢谢!
上一篇: JVM基础结构与字节码执行引擎
推荐阅读
-
像使用SQL一样对List对象集合进行排序
-
使用C++对物理网卡/虚拟网卡进行识别(包含内外网筛选)
-
C++类构造函数初始化列表 建议直接使用初始化列表为成员进行初始化实例教程
-
Effective Modern C++ 条款32 对于lambda,使用初始化捕获来把对象移动到闭包
-
Effective C++条款40:继承与面向对象之(明智而审慎地使用多重继承)
-
Effective C++笔记之四:确定对象被使用前已先被初始化
-
使用Intent进行跳转时传递对象;跳转到Activity时传递对象
-
java 使用面向对象方式实现录入学生信息,取出成绩最大值、最小值、平均值、对其进行排序
-
visual studio C++ 使用OpenMP 进行并行计算
-
C++编程思想 - 对象的创建和使用