1.5.3 参数的合法性检查
很多细致的程序员会在每个函数的入口处检查所有传入参数的合法性,尤其是指针。比如,下面的函数会销毁一个映射表:
int pcutils_map_destroy(pcutils_map* map) { if (map == NULL) return -1; pcutils_map_clear(map); free(map); return 0; }
该函数首先判断传入的参数map
是否为空指针。可以预期,传入该函数的参数map
是由名为pcutils_map_create()
的函数返回的。作为创建对象的函数接口,一般返回空值(NULL
指针)表示失败,返回非空值则表示成功;如果pcutils_map_create()
函数返回空值,则不用再调用pcutils_map_destroy()
函数。换句话说,在调用pcutils_map_destroy()
函数时,除非误用,否则不会给这个函数传递一个空值。
因此,这种判断貌似有必要,但仔细考虑后就会发现意义不大。在上面的代码中,程序将NULL
作为非法值做了特别处理,但如果传入的指针值为1或者−1,它们显然也是非法值,那为何不对这两种情况做判断并返回对应的错误值呢?更进一步地,如何判断一个尚未分配的地址值呢?
实质上,C语言并没有提供任何能够判断一个指针的值是否合法的语言级能力或者机制。我们所知道的不合法的指针值通常就是0、−1,以及特定情况下和当前处理器的位宽不对齐的整数值。比如在32位系统中,对于指向32位整数的指针来讲,任何不能被4整除的指针值大概率是非法的。除此之外,我们没有其他有效的手段来判断一个指针值的合法性。因此,这类参数的有效性检查其实是多余的。
再者,在频繁调用的函数中执行此类不必要的参数有效性检查,会大大降低程序的执行效率。
因此,上述代码的最佳实现应该如下:
void pcutils_map_destroy(pcutils_map* map) { pcutils_map_clear(map); free(map); }
我们没有必要仅针对空值做参数的有效性检查。一方面,这种检查并不能覆盖所有的情形;另一方面,如果我们仅仅需要检查空值这种情形,那么程序会很快因为访问空指针而出错。后一种情况说明调用者误传了参数,在程序的开发阶段,借助调试器,我们可以迅速定位缺陷所在。
但在某些情况下,我们仍然希望在调用这类函数时,对传入的常见非法值NULL
做一些特殊处理,以便可以及时发现调用者的问题。为此,我们可以使用assert()
。assert()
本质上是一个宏,而非函数,而且这个宏的行为依赖于NDEBUG
宏。assert()
通常的定义如下:
#ifdef NDEBUG # define assert(exp) \ do { \ } while (0) #else /* defined NDEBUG */ # define assert(exp) \ do { \ if (!(exp)) \ abort(); \ } while (0) #endif /* not defined NDEBUG */
在上面的代码中,NDEBUG
是一个约定俗成的全局宏,通常由构建系统定义。当NDEBUG
宏被定义时,意味着程序将被构建为发布版本,assert()
不做任何事情;反之,当程序被构建为调试版本时,assert()
将判断表达式exp
的真假,若为假,则调用abort()
函数终止程序的运行。
如此一来,我们可以将上述代码进一步修改为如下形式:
#include <assert.h> void pcutils_map_destroy(pcutils_map* map) { assert(map != NULL); pcutils_map_clear(map); free(map); }
此外,还有一种针对参数的合法性检查,或者说针对常规条件分支的优化方法,常见于一些优秀的C语言开源项目中。程序清单1.4列出了glib(Linux系统常用的C工具函数库,在一些场景中也可写作GLib)中用于快速验证UTF-8编码有效性的函数。
程序清单1.4 使用UNLIKELY
宏优化条件分支
#define VALIDATE_BYTE(mask, expect) \ do { \ if (UNLIKELY((*(uint8_t *)p & (mask)) != (expect))) \ goto error; \ } while (0) /* see IETF RFC 3629 Section 4 */ static const char * fast_validate(const char *str) { size_t n = 0; const char *p; for (p = str; *p; p++) { if (*(uint8_t *)p < 128) { n++; } else { const char *last; last = p; if (*(uint8_t *)p < 0xe0) { /* 110xxxxx */ if (UNLIKELY (*(uint8_t *)p < 0xc2)) goto error; } else { if (*(uint8_t *)p < 0xf0) { /* 1110xxxx */ switch (*(uint8_t *)p++ & 0x0f) { ... } } else if (*(uint8_t *)p < 0xf5) { /* 11110xxx excluding out-of-range */ switch (*(uint8_t *)p++ & 0x07) { ... } p++; VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ } else goto error; } p++; VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ n++; continue; error: return last; } } return p; }
上述代码多次使用了UNLIKELY
宏,用于判断一些不太可能出现在正常UTF-8编码中的字符。这个宏以及成对定义的LIKELY
宏利用了现代编译器的一些特性,它们可以告诉编译器一个分支判断的结果为真或者为假的可能性是大还是小。利用这两个宏,我们可以协助编译器充分利用处理器的分支预测能力,提高编译后代码的执行效率。
因此,如果非要检查传入参数的有效性,我们可以利用UNLIKELY
宏,对旨在销毁映射表的代码作如下优化:
int pcutils_map_destroy(pcutils_map* map) { if (UNLIKELY(map == NULL)) return -1; pcutils_map_clear(map); free(map); return 0; }
这样编译器就会认为出现map == NULL
这一条件的可能性较低,从而在生成最终的机器指令时,通过适当的优化,将可能性较低的条件判断对性能的影响降到最小。
注意,LIKELY
和UNLIKELY
宏是非标准宏,目前仅GCC或兼容GCC的编译器支持。这两个宏通常定义如下:
/* LIKELY */ #if !defined(LIKELY) && defined(__GNUC__) #define LIKELY(x) __builtin_expect(!!(x), 1) #endif #if !defined(LIKELY) #define LIKELY(x) (x) #endif /* UNLIKELY */ #if !defined(UNLIKELY) && defined(__GNUC__) #define UNLIKELY(x) __builtin_expect(!!(x), 0) #endif #if !defined(UNLIKELY) #define UNLIKELY(x) (x) #endif
其中使用了__builtin_expect
这一GCC特有的优化指令。