捕获未经测试的返回值
前面翻译的一篇文章《使用错误代码对象进行C++错误处理》中提到,作者的灵感来源于另一篇文件《捕获未经测试的返回值》,于是再把这篇文章翻译过来,做为对比,也算是形成一个系列的文章。
前言
函数返回值通常用于表函数是否在没有错误的情况下执行。但是,很难确保调用者适当地使用这些信息(指返回值)。也许一些商业工具可以完成这项工作,但你并不总是能获得购买许可,特别是在小型项目中。你很有可能听到过这样说:“我相信你,你不会犯这样的错误。”
我在这里提出的想法,是受到几周前我们在项目中出现的一个错误的启发。它只出现在一个生产环境中,花了几天的时间才发现,它来自于一个特定环境的初始化例程的失败。实际上,调用此例程的代码没有测试其返回代码。
添加责任标志
在我的经验中(但不是很长时间),我经常看到函数返会是按类型分组成一个枚举。如图1所示,调用者可以或多或少忽略此类返回值。为了控制返回值发生了什么,我不直接返回这些值,而是返回类ErrorCode的实例。它包含两个成员变量:值(enValue_)指示函数错误代码,责任标志(PboResp_):
class ErrorCode
{
private:
ErrorCodeValue enValue_;
bool * PboResp_;
public:
// some code
}
责任标志的目的是表明是否需要对ErrorCode对象的值负责(指是否需要检查)。当ErrorCode对象使用拷贝构造函数或赋值函数拷贝对象时,值(enValue_)被拷贝,同时责任标志PboResp_中包含的责任需要“转移”。所谓“转移”,是指拷贝操作将责任从源实例转移到目标实例。复制后,源实例不再负责其内容(即调用者不再需要检查)。(实际上,拷贝构造函数的语义与赋值函数略有不同,但总体思路是相同的。下文详解。)
因为拷贝构造函数和赋值函数的参数使用const修饰符,我选择将责任标志PboResp_实现为一个指向布尔值的指针,而不是一个布尔值。这使拷贝函数能够修改作为参数传递进来的源实例的责任标志PboResp_。还有另一个适用于运算符=约束:如果参数ErrorCode对象的责任标志PboResp_是true,使之前值enValue_会丢失,必须要记录(指输出日志)。(此处描述的责任转移过程类似于发生在auto_ptr上的复制)
ErrorCode对象还要使用==和!=运算符。与从函数返回的临时ErrorCode对象比较时,需要这些操作符。这些临时ErrorCode对象已被构造成表示特定的错误状态(例如成功)。如您所料,这些操作符函数就是比较两个ErrorCode对象内部的值enValue_。而且,这些ErrorCode对象的责任标志PboResp_也要设置为false,这样就不会出现“未经测试的错误代码”的日志记录。如有需要,其他测试操作符(<, >)也可以实现。
最后,析构函数要检查实例是否仍然对错误代码值负责(责任标志PboResp_为true),如果存在这种情况就要记录它。
与现有代码的集成
在我看来,这项技术的成功取决于它能否很容易地集成到现有的程序中。图1展示了一种情况,这是一个很好的起点,可以演示如何进行集成。
首先,在类定义时,我使用之前定义的枚举的名称作为类名(即旧的枚举名是ErrorCode,则新的类名就是ErrorCode)。因此,所有之前返回枚举值的函数,现在都变成返回一个错误码对象,只要重新编译程序即可。我还将现在有枚举名ErrorCode改为ErrorCodeValue。要使这项技术真正有效,还必须定义一个ErrorCodeValue作为参数的构造函数。此构造函数主要在两种情况下会被隐式调用:第一,之前函数是返回ErrorCodeValue,而不是ErrorCode类的对象。第二,当ErrorCode对象和ErrorCodeValue进行比较(通过运算符==或!=)。在这种情况下,会用ErrorCodeValue构造出一个临时的ErrorCode对象,用作比较运算符的参数。如前所述,比较运算符还“关闭”两个对象的责任标志(责任标志PboResp_置为true)。
实施
图2显示错误处理的新实现,即使用ErrorCode类。ErrorCode类添加到现有文件ErrorCodes.h中。此外,还需要创建一个文件ErrorCodes.cpp,用于实现ErrorCode类的成员函数。
如上所述,拷贝构造函数和赋值操作符必须将错误代码的责任转移到它们的目标对象。但这还不够,函数必须始终ErrorCode对象,该对象负责其内容。因此,拷贝构造函数和接受ErrorCodeValue的构造函数,在构建对象时将责任标志PboResp_设置为true(他们无条件地这样做)。这就是rf构造函数与赋值操作符的不同之处:赋值操作符把责任标志从源对象复制到目标实例的,拷贝构造函数则要设置责任标志为true。
最后,默认构造函数与所有其他构造函数不同,因为它将责任标志初始化为false。默认构造的ErrorCode对象不表示未经测试的错误代码,它完全取决于创建它的程序员来决定如何处理它。
增强版的ErrorCode类,可以在析构函数和赋值操作符函数中增加断言,以便在开发和测试阶段拦截“错误泄漏”(即知道有哪些错误码没有检查)。
把新的错误码类在图1程序中实施,运行后将在标准错误输出上输出以下消息:
Destruction of untested error code:
value 1
Untested error code (value 0) erased by
new value 2
结语
我相信这个编码技巧对于检测某种类型的错误是非常有帮助的。请注意,此方法没有消除任何错误,只是在问题发生后报告未经测试的返回代码。我相信即使在现有的项目中也很容易实现。开始时,你可能会得到一个日志文件,其中包含大量未经测试的返回代码。因此,在我们的项目中,我们不得不稍微清理一下代码。
欢迎关注我的公众号【林哥哥的编程札记】,也欢迎赞赏,谢谢!