使用断言在软件开发阶段及测试阶段,可帮助我们检查程序中的一些错误,并提供有用的信息。
常用的断言有两种:一种是动态断言,即大家所熟知的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宏调用的函数内部。
通过上述事例,我们可以看出合理的实现自定义的断言。可实现断言满足我们的期望的行为。为我们提供便利。
请谨记
- 合理的使用断言,以提升程序开发和问题定位的效率。