PRE31-C. 在非安全的宏中避免使用有副作用的参数
非安全的函数宏在展开时会导致它的参数会被评估多次或不评估。永远不要触发参数中包含复制、自增、自减、volatile访问、input/output或者有其他副作用(包括函数调用)的宏。
关于非安全宏的文档中应当给出警告,不要使用带有副作用的参数触发宏。但主要责任还在于使用宏的编程人员。因为使用上的风险,所以建议避免创建非安全的函数宏。(参见 PRE00-C. Prefer inline or static functions to function-like macros)
这条规则和 EXP44-C. Do not rely on side effects in operands to sizeof, _Alignof, or _Generic类似。
不遵从规范的示例代码
不安全宏的一个问题就是宏参数的副作用,如下所示:
#define ABS(x) (((x) < 0) ? -(x) : (x))
void func(int n) {
/* Validate that n is within the desired range */
int m = ABS(++n);
/* ... */
}
上面例子中触发ABS()宏会按如下展开:
m = (((++n) < 0) ? -(++n) : (++n));
n会被自增两次而不是一次。
遵从规范的解决方案
在这个遵从规范的解决方案中,自增操作 ++n
在调用非安全宏之前被执行。
#define ABS(x) (((x) < 0) ? -(x) : (x)) /* UNSAFE */
void func(int n) {
/* Validate that n is within the desired range */
++n;
int m = ABS(n);
/* ... */
}
注意注释里提醒了编程人员该宏是不安全的。这个宏可以被命名为 ABS_UNSAFE()
来明确提醒该宏是非安全的。这个符合规范的解决方案,也存在着未定义行为,当传入ABS()
的参数等于有符号整数的最小值时候(最小的负值). (参见 INT32-C. Ensure that operations on signed integers do not result in overflow)
遵从规范的解决方案
这个解决方案遵从了 PRE00-C. Prefer inline or static functions to function-like macros ,通过定义内联函数iabs()
来替代宏ABS()
. ABS()
宏可以操作任意类型的操作数,但abs()
函数会将比int
类型宽的参数截断掉,使参数值的范围在int
类型范围内。
#include <complex.h>
#include <math.h>
static inline int iabs(int x) {
return (((x) < 0) ? -(x) : (x));
}
void func(int n) {
/* Validate that n is within the desired range */
int m = iabs(++n);
/* ... */
}
遵从规范的解决方案
一个更灵活的解决方案是在声明ABS()
宏时使用_Generic
关键字。为支持所有算数类型,这个解决方案同时使用了内联函数来计算整型的绝对值。(参见PRE00-C. Prefer inline or static functions to function-like macros 和PRE12-C. Do not define unsafe macros.)
根据C语言标准 6.5.1.1, 第3段 [ISO/IEC 9899:2011]:
generic selection中的控制表达式不被评估. 如果一个generic selection有一个generic association的类型和控制表达式的类型相容,那么the generic selection表达式的结果就是generic association的表达式中的结果。否则,generic selection 表达式的结果是
default
generic association表达式的结果。其余 generic association表达式不被评估。
因为 generic selection的控制表达式不被评估,所有这个解决方案中保证了宏参数v
只被评估一次。
#include <complex.h>
#include <math.h>
static inline long long llabs(long long v) {
return v < 0 ? -v : v;
}
static inline long labs(long v) {
return v < 0 ? -v : v;
}
static inline int iabs(int v) {
return v < 0 ? -v : v;
}
static inline int sabs(short v) {
return v < 0 ? -v : v;
}
static inline int scabs(signed char v) {
return v < 0 ? -v : v;
}
#define ABS(v) _Generic(v, signed char : scabs, \
short : sabs, \
int : iabs, \
long : labs, \
long long : llabs, \
float : fabsf, \
double : fabs, \
long double : fabsl, \
double complex : cabs, \
float complex : cabsf, \
long double complex : cabsl)(v)
void func(int n) {
/* Validate that n is within the desired range */
int m = ABS(++n);
/* ... */
}
Generic selections在C11中被引入,在C99和之前的C语言标准中没有。
遵从规范的解决方案 (GCC)
GCC的 __typeof
扩展特性可以声明并将宏操作数的值赋给一个同类型的临时变量,并在临时变量上进行计算,这样保证了操作数只被评估一次。另外一个GCC的扩展特性, statement expression,可以在表达式需要的地方创造一个块block statement。
#define ABS(x) __extension__ ({ __typeof (x) tmp = x; \
tmp < 0 ? -tmp : tmp; })
注意这个使用这个扩展特性使代码变得不具有可移植性,违反了 MSC14-C. Do not introduce unnecessary platform dependencies
不遵从规范的示例代码(assert()
)
assert()
宏是在代码中引入诊断检查的一种方便机制(参见 MSC11-C. Incorporate diagnostic tests using assertions)。作为assert()
宏参数的表达式不应该有副作用。assert()
宏的行为取决于NDEBUG
宏的定义。如果NDEBUG
宏没有被定义,assert()
宏会评估它的表达式参数,并将结果和0比较,如果结果等于0,就调用abort()
函数。如果NDEBUG
宏没有被定义,assert
被定义为((void)0)
。结果是在断言中的表达式不会被评估,也不会像非debug版本中那样发生副作用。
这个示例代码中包含了一个assert()
宏,它的表达式(index++)
有副作用:
#include <assert.h>
#include <stddef.h>
void process(size_t index) {
assert(index++ > 0); /* Side effect */
/* ... */
}
遵从规范的解决方案 (assert()
)
这个方案通过将表达式移动到assert()
宏的外部,从而避免了断言中的副作用。
#include <assert.h>
#include <stddef.h>
void process(size_t index) {
assert(index > 0); /* No side effect */
++index;
/* ... */
}
风险评估
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
PRE31-C | Low | Unlikely | Low | P3 | L3 |
参考文献
[Dewhurst 2002] | Gotcha #28, "Side Effects in Assertions" |
---|---|
[ISO/IEC 9899:2011] | Subclause 6.5.1.1, "Generic Selection" |
[Plum 1985] | Rule 1-11 |
网友评论