实用经验 93 合理的使用断言(ASSERT)

使用断言在软件开发阶段及测试阶段,可帮助我们检查程序中的一些错误,并提供有用的信息。

常用的断言有两种:一种是动态断言,即大家所熟知的C标准库的assert()宏,一种是C中的静态断言,即在编译期间检查。

动态断言:assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义如下:

#include <assert.h> 
void assert( int expression ); 

assert作用是先计算表达式expression,如果其值为假(即为0),那么它先向stderr打印一条错误信息,然后通过调用abort来终止程序运行。

assert是运行期判断,并且会强制终止程序,一般要求只能用于debug版本,目的是尽可能快的发现问题。尤其在我所从事的电信软件产品中,assert需要从release版本中去掉。所以一般开发会重新定义assert宏。这原因很简单,电信软件运行电信、移动等运营商的机房。不能已出现问题就直接宕机,否则移动终端用户会投诉运营商的。

静态断言,在新的C 标准中C0x中,加了对静态断言的支持,引入了新的关键字static_assert来表示静态断言。使用静态断言,我们可以在程序的编译期,检测一些条件是否成立。我们看一个简单静态断言实现:

// 声明一个StaticAssert模板类 
template <bool assertion> struct StaticAssert; 
// 偏特化此模板,另assertion = true
template <> struct StaticAssert<true> 
{ 
	enum { VALUE = 1 }; 
}; 

#define STATIC_ASSERT(expression)    StaticAssert<expression>::VALUE

我们先说明一下实现原理:先声明一个模板类,但后面仅偏特化参数值为true的类,而为false的类声明为未定义的类,即是一个不完整的类型,编译期间无法找到StaticAssert::VALUE类型。举例如下:

STATIC_ASSERT(4 == sizeof(long) ); //在 32bit机上OK 
STATIC_ASSERT(4 == sizeof(long) ); //在 64bit机上NG,long为8字节 
STATIC_ASSERT(4 == sizeof(long) ); //在 32bit机上OK
STATIC_ASSERT(4 == sizeof(long) ); //在 64bit机上NG,long为8字节

静态断言在编译时进行处理,不会产生任何运行时刻空间和时间上的开销,这就使得它比assert宏具有更好的效率。另外比较重要的一个特性是如果断言失败,它会产生有意义且充分的诊断信息,帮助程序员快速解决问题。

这里我们主要讨论动态断言。一个断言,它越“笨”,当它被触发时所能带给你的信息就越多——并且它也就越有价值。这是因为根据信息的理论,事件中的信息随着事件发生概率的提高而减少。某个assert越不可能触发,当它触发时带给你的信息也就越多。比如,当调试一些不带断言的代码时,你可能首先检查更明显的失败可能原因,你会只在晚些时候才考虑到那些“不可能发生”的条件。

有一种有效的测试手段来区分哪种场合你需要使用assert,什么时候需要使用真正的错误检查:你对可能发生的情况使用错误检查,即使这些情况不太可能发生。你只对你确信在任何情况下都不可能发生的情况使用assert。一个失败的断言总是指明一个设计上或程序员的错误——不是一个用户错误。

断言使用规则

  • 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
  • 在函数的入口处,使用断言检查参数的有效性(合法性)。
  • 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
  • 很多此类的书籍都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

断言检测的情况理论上几乎总是可以在编译时检测。但仅仅是理论上。验证某些事情实际上是行不通的,比如不可接受的编译时间,缺乏源代码等等。

另外,你不会用断言检查可能失败的函数返回值。你不会用assert来确保malloc工作正常,创建成功一个窗口,或者启动一个线程。但是你能够用assert来保证API如文档中所写的那样工作。比如,如果某个API函数的文档说它只返回一个正值,但由于某种原因你觉得它可能存在问题,你就需要写一条assert。

标准提供的assert有着非常简单的实现,类似于下面:

//来自于标准包含文件cassert或assert.h
#ifdef NDEBUG
	void __assert(const char*, const char*, int);
	#define assert(e) ((e) ? (void)0 : __assert(#e, __FILE__, __LINE__))
#else
	#define assert(unused) ((void)0)
#endif

__assert辅助函数在标准错误输出流中显示一条错误信息并且通过调用abort()来中断程序。当然各个编译器其实现的方式也不尽一致。

实际上问题还远非如此。有时候,为了达到某种标准assert无法达到目的。你必须自己实现一个assert函数。当然这其中的原因是多种多样的,例如:你可能觉得用abort()终止程序过于粗鲁。很多情况下,你可能想要忽略一个特殊断言,因为你觉得它是无害的。

某些操作系统和调试器允许你进入你的源代码来追踪发生的问题,同样在这种情况下你不需要abort(),相反你需要有选择是否继续跟踪程序代码的自由。你可以自己实现assert函数。在VC环境下,下面这个断言实现即可满足上述要求:

// VC的专有指令,调用调试器。
#define BREAK_HERE __asm { int 3 } 
// 自定义的Assert函数实现。
inline void Assert (const char* e, const char * file, int line)
{
	switch (AskUser(e, file, line))
	{
	case giveUp:
		abort ();
		break;
	case debug:
		BREAK_HERE;
		break;
	case ignore:
		break;
	default:
	    break;
	}
}

#define ASSERT(e)  ((e) ? (void)0 : Assert(#e, __FILE__, __LINE__))

AskUser函数使用某种I/O来询问用户程序需要采取何种行动。一个更好的断言机制还会提供在程序终止,调试或忽略前记录日志的选择。

BREAK_HERE的一个问题是它经常在被调用的精确位置中断进入调试器。但你需要的代码中断位置是你调用ASSERT的位置,而不是AskUser定义内部。这要求你在ASSERT宏内部插入BREAK_HERE,而不是在ASSERT宏调用的函数内部。

通过上述事例,我们可以看出合理的实现自定义的断言。可实现断言满足我们的期望的行为。为我们提供便利。

请谨记

  • 合理的使用断言,以提升程序开发和问题定位的效率。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值