原文发表于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

欢迎关注我的公众号【林哥哥的编程札记】,也欢迎赞赏,谢谢!

使用错误代码对象进行C++错误处理的更多相关文章

  1. 发生了Post错误:错误代码40005,微信返回错误信息:invalid file type

    给客户部署 PxxCms, 使用群发功能发送图文的的时候提示: 发生了Post错误:错误代码40005,微信返回错误信息:invalid file type, 没学过php伤不起 ... Google ...

  2. http 错误代码解释 && nginx 自定义错误【转】

    如果向您的服务器发出了某项请求要求显示您网站上的某个网页(例如,当用户通过浏览器访问您的网页或在 Googlebot 抓取该网页时),那么,您的服务器会返回 HTTP 状态代码以响应该请求. 此状态代 ...

  3. javascript jquery封装对象时的错误,求解!我想知道为什么

    jquery   封装对象时的错误 --------------------------------------------<input id="name" name=&qu ...

  4. (转)解决fasterxml中string字符串转对象json格式错误问题(无引号 单引号问题)

    原文地址:解决fasterxml中string字符串转对象json格式错误问题 com.fasterxml.jackson.databind.ObjectMapper mapper = new com ...

  5. 跨越DLL边界传递CRT对象潜在的错误

    跨越DLL边界传递CRT对象潜在的错误 翻译:magictong(童磊)2013年5月 版权:microsoft 原文地址:http://msdn.microsoft.com/en-us/librar ...

  6. Spring mvc 下Ajax获取JSON对象问题 406错误

    我在学习springmvc过程中(我的项目是配置的后缀是.html),从controller返回对象. 如果我不使用 mvc-annotation-driver,而是手动配置,AnnotationMe ...

  7. Appium使用PageFactory初始化对象时报空指针错误

    自己的测试框架里面,每个app页面都要初始化appium field,所以想到使用一个静态的变量,后来初始化一个页面对象时总是报空指针. 在网上找了好多材料,看着没有什么区别.后来在github上面看 ...

  8. 无法为请求的 Configuration 对象创建配置文件 错误原因

    Configuration config = WebConfigurationManager.OpenWebConfiguration("~"); 无法为请求的 Configura ...

  9. 解决ASP.NET Web API Json对象循环参考错误

    前言 一般我们在开法 ASP.NET Web API 时,如果是使用 Entity Framework 技术来操作数据库的话,当两个 Entity 之间包含导览属性(Navigation Proper ...

随机推荐

  1. JS中面向对象中的继承(常用写法)---核心部分

    1.基本概念 子类继承父类,但是不能影响父类.包括1.混合继承(构造函数+原型) 2.ES6新增class的继承. 接下来介绍,面向对象中继承的两种常用写法.即混合继承(构造函数+原型)和class继 ...

  2. xcode制作越狱包

    1.将运行目标选为iOS Device 2.Edit Scheme -> 选择 Run [App Name] -> Build Configuration下拉框中选择Release 3.生 ...

  3. 轻量级MVC框架(自行开发)

    源码及demo: https://github.com/killallspree/myFrame/

  4. PG归并排序算法详解

    前言 归并排序算法是连接算法中比较复杂的算法,相比嵌套循环与Hash匹配而言.本节会通过实例来说明该算法在PG中的具体实现. 在PG中,通过状态机来实现--归并-连接.当然这里的完整流程是排序--归并 ...

  5. CSS重构:样式表性能调优

    这两天窝在家里又看了本CSS相关的书:<CSS重构:样式表性能调优>.重构是指在不改变代码行为的前提下,重写代码,使其更加简洁.易于复用. 这本书读起来比较快,可挑自己感兴趣的读,前面三章 ...

  6. 记录一次云主机部署openstack的血泪史

    看见这个部署成功的留下了激动的泪水 经过长时间的BUG苦肝终于成功部署成功  部署的环境2vCPU 8GB 阿里云主机,部署成功以后内存占用确实蛮高的 记录这一次踩坑,给后来者避免踩坑时间,个人踩坑踩 ...

  7. scapy学习笔记

    1.ACK Scan >>>ans,unans=sr(IP(dst="www.baidu.com")/TCP(dport=[80,666],flags=" ...

  8. 进制-Iterative-进制转换

    2019-12-02 21:15:31 进制转换是计算机科学里的一个基础算法,通常可以使用如下的模版来进行计算. 下面我们来讨论一些关于进制的题目. 1271. Hexspeak  问题描述: 问题求 ...

  9. 从源码和doc揭秘——Java中的Char究竟几个字节,Java与Unicode的关系

    #编码与字符编码 (懂编码的建议直接跳过) 在计算机世界中,任何事物都是用二进制图片数字表示的,图片可以编码为JPG,PNG格式的字节流,音频,视频有MP3,MP4格式的字节流.这些JPG,MP3等都 ...

  10. Centos单机部署Elasticsearch7.2集群

    配置node0 # ======================== Elasticsearch Configuration ========================= # # NOTE: E ...