在玩
__VA_OPT__(,)
时,我注意到以下行为。
首先,请注意,C 可以在初始值设定项中使用尾随逗号。这些是等效的:
int foo[] = { 10, 20, 30 };
int baz[] = { 10, 20, 30, };
所以让我们设计一个初始化宏,它也可以工作:
#define bar(...) { __VA_ARGS__ }
int foo[] = bar(10, 20, 30);
int baz[] = bar(10, 20, 30,);
现在我想将
40
添加到列表中,即使列表为空。这两种方法都有效; foo
得到 10, 20, 30, 40
并且 baz
得到 40
:
#define bar(...) { __VA_ARGS__ __VA_OPT__(,) 40 }
int foo[] = bar(10, 20, 30);
int baz[] = bar();
但是如果我们忘记了末尾多余的逗号怎么办?
#define bar(...) { __VA_ARGS__ __VA_OPT__(,) 40 }
int foo[] = bar(10, 20, 30, );
现在我们得到这个错误,这并不是非常直观。它从不直接指向
30
: 之后的“问题逗号”
r.c: In function ‘main’:
r.c:160:43: error: expected expression before ‘,’ token
160 | #define bar(...) { __VA_ARGS__ __VA_OPT__(,) 40 }
| ^
r.c:164:21: note: in expansion of macro ‘bar’
164 | int foo[] = bar(10, 20, 30,);
| ^~~
预处理器将逗号加倍:
# cpp r.c|grep -v ^#|indent
...
int foo[] = { 10, 20, 30,, 40 };
是否可以设计一个宏来防止逗号重复,或者调整宏以从编译器中产生更合理的错误?
上面的例子非常简单,但是当事情变得复杂时,你错过了一个逗号,那么错误就很可怕了。以此处为例。
(请注意:这不是一个XY问题问题,因为我真的想知道C宏是否可以抑制双逗号情况。请不要建议“修复”这个更复杂但仍然人为的问题,第二个例子。)
假设我们静态初始化递归树状结构:
struct menu
{
char *name;
struct menu **submenu;
};
#define MENU_LIST(...) (struct menu*[]){ __VA_ARGS__ __VA_OPT__(,) NULL }
#define MENU_ITEM(...) &(struct menu){ __VA_ARGS__ }
它提供了非常漂亮的函数式语法,如下所示...但是您能在下面的代码中发现产生此错误的“额外”逗号吗?如果您习惯尾随逗号有效,您可能看不到它:
r.c:11:65: error: expected expression before ‘,’ token
11 | #define MENU_LIST(...) (struct menu*[]){ __VA_ARGS__ __VA_OPT__(,) NULL }
| ^
r.c:113:25: note: in expansion of macro ‘MENU_LIST’
113 | struct menu *mymenu[] = MENU_LIST(
| ^~~~~~~~~
r.c:122:9: note: in expansion of macro ‘MENU_ITEM’
122 | MENU_ITEM(
| ^~~~~~~~~
r.c:124:36: note: in expansion of macro ‘MENU_LIST’
124 | .submenu = MENU_LIST(
| ^~~~~~
struct menu *mymenu[] = MENU_LIST( // line 113
MENU_ITEM(
.name = "planets",
.submenu = MENU_LIST(
MENU_ITEM( .name = "Earth" ),
MENU_ITEM( .name = "Mars" ),
MENU_ITEM( .name = "Jupiter" )
)
),
MENU_ITEM( // line 122
.name = "stars",
.submenu = MENU_LIST( // line 124
MENU_ITEM( .name = "Sun" ),
MENU_ITEM( .name = "Vega" ),
MENU_ITEM( .name = "Proxima Centauri" ),
)
),
MENU_ITEM(
.name = "satellites",
.submenu = MENU_LIST(
MENU_ITEM( .name = "ISS" ),
MENU_ITEM( .name = "OreSat0" )
)
)
);
但是如果我们忘记了末尾多余的逗号怎么办?
正如您所发现的,变量参数列表中的分隔符(但不是前面的分隔符)包含在变量参数中。预处理器在解析变量参数列表时实际上根本没有义务解析出各个参数——它只需要跟踪括号以便识别宏调用的结尾。变量参数及其分隔符都作为单个项目出现,直到执行重新扫描。
是否可以设计一个宏来防止逗号重复,或者调整宏以从编译器中产生更合理的错误?
没有直接的方法来内省变量参数以根据它们是否以逗号结尾来进行区分。我相信,如果您限制变量参数的数量,那么您可以构造一个宏堆栈来做出该决定,使用与可以计算变量参数相同的技巧。但这种治疗方法比疾病本身更糟糕。如果您需要允许任意数量的变量参数,则它不适用。也就是说,
当事情变得复杂时,你错过了一个逗号,那么错误就很可怕。
让这成为关于宏观使用风险的警示故事。在这种特殊情况下,使用宏模糊了初始化语法、函数语法和类函数宏语法之间的区别,从而使问题变得更加复杂。我想,我比大多数人更喜欢宏,而且我也玩过一些愚蠢的宏游戏。我比这里的许多人更能容忍他们。尽管如此,我还是说使用更少的宏和更多的函数。并且不要忽视“以上都不是”的可能性。