原文发表于, 由本人翻译整理分享于此.
前言
我已经使用了本文描述的代码和机制近 20 年了, 到目前为止, 我还没有找到更好的方法来处理大型 C++ 项目中的错误. 最初的想法是从一篇文章 (Dr Dobbs Journal 2000 年) 中摘录出来的. 我已经添加了一些新内容进去, 使它更容易在生产环境中使用.
写这篇文章的冲动是最近发表在 Andrzej 的 C++ 博客. 正如我们在本文后面将看到的那样, 使用错误代码对象可以产生更清晰, 更易于维护的代码.
背景
每个 C++ 程序员都知道处理异常情况的传统方法有两种: 第一种是从良好的旧 C 风格继承而来, 返回错误代码, 并希望调用者进行判断并采取适当的操作; 第二种方法是抛出异常, 并希望周围代码块捕获并处理该异常. C++ FAQ https://isocpp.org/wiki/faq/exceptions 强烈支持第二种方法, 认为它会使得代码更安全.
然而, 使用异常也有其自身的缺点. 代码变得更加复杂, 用户必须知道所有可能引发的异常. 这就是为什么旧的 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
来源: https://www.cnblogs.com/qinwanlin/p/12669347.html