PRE31-C. 在非安全的宏中避免使用有副作用的参数

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 
#include 
  
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 
#include 
  
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 
#include 
   
void process(size_t index) {
  assert(index++ > 0); /* Side effect */
  /* ... */
}

遵从规范的解决方案 (assert())

这个方案通过将表达式移动到assert()宏的外部,从而避免了断言中的副作用。

#include 
#include 
   
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

你可能感兴趣的:(PRE31-C. 在非安全的宏中避免使用有副作用的参数)