定义宏时使用逗号运算符

Using the comma operator when defining a macro

考虑逗号运算符适合使用 K&R 状态的情况(第 3 章,第 63 页)“...,以及在多步计算必须是单个表达式的宏中。"

现在我知道 #define SYMVAR (expr1, expr2, expr3)SYMVAR 设置为 expr3 但这并不是本书建议的用途。我的困惑源于所提到的逗号运算符的使用缺乏适当的例子;其次,只有当每个逗号分隔的表达式实际上以任何方式对宏本身的值做出贡献时,宏中的多步计算才是有目的的,只有当我们以某种方式将一些中间值存储在一些临时变量中时才有可能任何意义,因为我们正在谈论预处理器指令。

我错过了什么吗?书中描述的逗号运算符的这种用法的适当示例是什么?

想象一个函数

void debug_log(const char *s);

将指定的字符串(连同时间戳)写入某种日志文件。

那么你可以定义如下宏:

#define TWICE(X)  (debug_log("Calling TWICE(" #X ")"), (X) * 2)

TWICE(21) 将计算为 42,但也会写一条形式为

的消息
[2019-09-03 12:34:56] Calling TWICE(21)

到日志文件。

重点是 C 中的表达式可能有副作用。他们可以修改变量,可以写入文件等,即使他们的 return 值没有被使用。

melpomene 提供了一个例子。另一个例子,虽然你可以争论它是好是坏,但是如果你想在循环头中使用宏。

#define MACRO(X) (X--, X>0)

int x=5;
while(MACRO(x)) {
    // Do stuff
}

这个例子绝对不是世界上最好的,但关键是如果你用分号代替表达式来分隔,不管你如何封装它们都是行不通的。

由于逗号运算符语句的值是语句中最后一个逗号分隔的表达式,您可以使用它来执行多个表达式,同时只保留最后一个表达式的值。

例如,假设您想要一个可以进行多次计算的宏,但宏的值是最后一次计算。以下宏利用表达式中使用的赋值运算符来执行计算,并允许不同的计算作为宏的结果:

#define SYMVAR(a,b)  ( ((a)=(b)), (b)*2)

这也可以是一系列函数调用,例如:

int f1(int x) { return x * 3; }

int f2(int x) { return x * 4; }

// call first function f1() and then f2() with the value of the macro being the
// value returned by function f2().
#define SYMVARy(a) (f1(a), f2(a))

使用此方法的示例:

#include <stdio.h>

#define SYMVAR(a,b)  ( ((a)=(b)), (b)*2)

#define SYMVAR2(a,b,z)  ( ((a)=(b)), ((z)=(b))*2)

int main()
{
    int n1, n2;
    int x1, x2;
    int y0;
    int i;

    for (i = 0; i < 10; i++) {
        printf(" i = %d  -> ", i);
        x1 = SYMVAR(n1, i);
        printf(" n1 = %d, i = %d, x1 = %d\n", n1, i, x1);
    }

    printf("\nloop 2\n");
    for (i = 0; i < 10; i++) {
        printf(" i = %d  -> ", i);
        x2 = SYMVAR2(n2, i+3, y0);
        printf(" n2 = %d, i = %d, x2 = %d, y0 = %d\n", n2, i, x2, y0);
    }

    return 0;
}

这个程序的输出是:

 i = 0  ->  n1 = 0, i = 0, x1 = 0
 i = 1  ->  n1 = 1, i = 1, x1 = 2
 i = 2  ->  n1 = 2, i = 2, x1 = 4
 i = 3  ->  n1 = 3, i = 3, x1 = 6
 i = 4  ->  n1 = 4, i = 4, x1 = 8
 i = 5  ->  n1 = 5, i = 5, x1 = 10
 i = 6  ->  n1 = 6, i = 6, x1 = 12
 i = 7  ->  n1 = 7, i = 7, x1 = 14
 i = 8  ->  n1 = 8, i = 8, x1 = 16
 i = 9  ->  n1 = 9, i = 9, x1 = 18

loop 2
 i = 0  ->  n2 = 3, i = 0, x2 = 6, y0 = 3
 i = 1  ->  n2 = 4, i = 1, x2 = 8, y0 = 4
 i = 2  ->  n2 = 5, i = 2, x2 = 10, y0 = 5
 i = 3  ->  n2 = 6, i = 3, x2 = 12, y0 = 6
 i = 4  ->  n2 = 7, i = 4, x2 = 14, y0 = 7
 i = 5  ->  n2 = 8, i = 5, x2 = 16, y0 = 8
 i = 6  ->  n2 = 9, i = 6, x2 = 18, y0 = 9
 i = 7  ->  n2 = 10, i = 7, x2 = 20, y0 = 10
 i = 8  ->  n2 = 11, i = 8, x2 = 22, y0 = 11
 i = 9  ->  n2 = 12, i = 9, x2 = 24, y0 = 12

此方法的局限性

此方法的一个限制是无法创建范围为逗号表达式本身的临时变量。

解决方法是使用 do { … } while(0) 构造,但是您会遇到宏在赋值中不再有效的问题。

一些其他注意事项

这种东西使用起来有点棘手。您必须始终记住,C 预处理器正在执行文本替换,并且没有太多智能。

您应该使用括号来强制执行特定的计算顺序,并确保使用宏时提供的替换文本将导致正确的操作顺序((i + 3) * 2 而不是 i + 3 * 2由于运算符的优先顺序,这是两个不同的语句。

另请注意,在多次使用宏参数的宏中使用具有副作用的表达式可能会导致意想不到的后果。例如:

#define SYMVAR(a,b)  ( ((a)=(b)), (b)*2)

int xA, xB = 2, xVal;

xVal = SYMVAR(xA, xB++);    // macro argument has a side effect, post increment

会导致 xVal 的值为 6(xB 递增一次,因为 post 递增,然后乘以 2),xA 有一个值2(xB 的原始值,因为使用 post 递增)和 xB 值为 4(xB 递增两次)。

您可以像这样使用上述宏之一:

xx SYMVAR(n1, n2);

编译时会出现警告,然后在 link 期间失败。 Visual Studio 2017 当我将上述语句添加到上述程序时,我看到以下警告和错误。请注意,这些警告和错误都是关于缺少外部函数 xx 而不是您可能期望的编译器错误。

Severity    Code    Description Project File    Line    Suppression State
Warning C4013   'xx' undefined; assuming extern returning int   console_scan    d:\users\rickc\documents\vs2017repos\console_scan\console_scan\source_macro.c   27  
Error   LNK2019 unresolved external symbol _xx referenced in function _main console_scan    D:\Users\rickc\Documents\vs2017repos\console_scan\console_scan\Source_Macro.obj 1   
Error   LNK1120 1 unresolved externals  console_scan    D:\Users\rickc\Documents\vs2017repos\console_scan\Debug\console_scan.exe    1