虽然我个人是很不喜欢宏这个东西,但是它确实在C语言编程中非常常见,越底层被用的越多。
一、什么是副作用?
副作用是指表达式的求值除了计算出一个结果外还修改了程序状态的操作。例如,改变变量的值、进行输入输出操作等都是副作用的例子。在C语言中,副作用通常出现在赋值运算符、增量和减量运算符(如++
和--
)、函数调用以及逗号运算符中。
二、不安全宏的问题
宏是一种简单的文本替换机制,在预处理阶段进行替换,不会像函数那样检查参数类型或数量。如果宏的参数有副作用,而这个宏又多次使用该参数或者参数的求值顺序不确定,那么副作用就会被执行多次或在不可预期的时间点执行,这可能导致难以调试的错误。
三、示例
考虑以下宏定义:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
如果我们将具有副作用的表达式作为参数传递给这个宏,比如MAX(x++, y++)
,那么根据条件运算符(?:
)的工作方式,x++
和y++
中的一个将会被执行两次,具体哪个取决于比较的结果。这种行为是未定义的,并且会导致代码的行为不符合预期。
#define ABS(x) (((x) < 0) ? -(x) : (x))
void func(int n)
{
/* Validate that n is within the desired range */
int m = ABS(++n);
/* ... */
}
实际上int m = ABS(++n);
会被怎样展开呢?
m = (((++n) < 0) ? -(++n) : (++n));
其实这很可能不是我们想要的效果!
再比如,assert
是个宏,那么在使用assert
时出现副作用也是不安全的。
#include <assert.h>
#include <stddef.h>
void process(size_t index) {
assert(index++ > 0); /* Side effect */
/* ... */
}
四、如何避免?
为了避免这种情况,应该确保传递给宏的参数没有副作用。
可以通过下面几种方法来实现这一点:
- 避免使用带有副作用的表达式作为宏参数,比如增量/减量操作符、赋值语句或其他任何会改变状态的操作。
- 将复杂的表达式封装进临时变量中,然后将临时变量传递给宏。
- 重写宏为内联函数,这样可以利用编译器提供的更严格的检查,保证参数只被求值一次。
五、安全代码示例
5.1将副作用操作提前
#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);
/* ... */
}
5.2通过内联替代宏
#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);
/* ... */
}
5.3泛型
老生常谈的话题了
#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);
/* ... */
}
5.4gcc扩展__typeof
gcc
的 __typeof__
扩展使我们可以声明一个与宏操作数相同类型的临时变量,并将该操作数的值赋给临时变量,在这个临时变量上执行计算,因此保证操作数将被恰好求值一次。另一个 gcc
扩展,即所谓的语句表达式(statement expression),允许块语句出现在需要表达式的地方。
#define ABS(x) __extension__ ({ __typeof (x) tmp = x; \
tmp < 0 ? -tmp : tmp; })
更多精彩技术干货和技术资料,关注公众号“系统编程语言”。