在玩__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 };

问题

有没有可能设计一个宏来防止逗号加倍,或者调整宏以从编译器中产生更合理的错误?

More complicated example

上面的例子非常简单,但是当事情变得复杂时,并且您遗漏了一个逗号,那么错误就很可怕了.以here为例.

(Please note:这不是XY problem个问题,因为I really do want to know if C macros can suppress double-comma situations.请不要对这个更复杂但仍是人为设计的第二个例子提出"修复"建议.)

假设我们正在静态初始化递归的树状 struct :

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" )
            )
    )
);

推荐答案

@John Bollinger的答案可能是正确的:不要这样做.您最终会得到一堆糟糕的宏粘性,这比仅仅键入所有硬编码的初始值设定项并维护初始值设定项列表要难十倍.

尽管如此,在经历了一些头痛之后,GCC虫遇到了真正邪恶的宏观try ,这是世界上永远看不到的,我设法总结了一个合理的变通办法,几乎可以阅读:

  • 关键是使用指定的初始值设定项.它们附带了一个方便的技巧,即在每次使用时更改初始化器列表中使用的"当前对象".因此,如果我们可以通过首先将新的最后一项设置为我们的"哨兵值"(40NULL或其他值)来初始化数组,然后从零开始初始化其余的项.我们不必关心尾随的逗号,因为它将最后结束.

  • 我们如何知道未知大小的初始化列表中最后一项的索引?如果我们知道项目类型,则此宏计算传递的项目数:

    #define COUNT_ARGS(...) ( sizeof((int[]){__VA_ARGS__}) / sizeof(int) )
    

    该宏构建了一个与项数相对应的复合文字数组,然后我们只需将其除以预期类型的大小.(如果你想让它的类型安全,你可以偷偷地在某个地方放一个_Generic.)

  • 请注意,根据C23,允许空数组初始值设定项列表{}.然而,如果我们因为编号__VA_ARGS__而得到sizeof((int[]){});,那么这是一个零大小的数组,这是无效的C和一个令人讨厌的GNU扩展.这个GNU的垃圾绊倒了我,因为Buggygcc -std=c23 -pedantic-errors让它静静地滑倒,尽管它是不符合C的.每日的GCC错误.

    无论如何,要坚持标准C,我们必须想出一个变通办法:

     #define COUNT_ARGS(...) ( 0 __VA_OPT__(+ sizeof((int[]){__VA_ARGS__}) / sizeof(int)) )
    

    也就是说:如果有可变参数列表,则执行SIZOF技巧0 +.否则,如果没有变量参数,则使用__VA_OPT__丢弃除零以外的所有参数.我们将以指定的初始值设定项[0]结束,我们在其中分配前哨,这很好,因为这将创建一个单一对象的array.

  • 因此,我们要用来插入哨值的指定初始值将如下所示:

    [COUNT_ARGS(__VA_ARGS__)] = SENTINEL
    

    这将使索引1项大于传递的初始化器的数量,从而将数组大小扩展1.(假设打电话的人确实写了int arr[] = ....)在空参数列表的情况下,上面的0 +技巧意味着我们将项目索引分配为零,即第一个.

  • 我们将指定的初始值设定项放在第一位.则数组初始值设定项列表的其余部分变为[0] = __VA_ARGS__.如果我们通过了1,2,3,那么这个数字就会扩大到[0]=1,2,3.方便的是[0]将初始化列表的"当前对象"设置为索引0.在本例中,将该值初始化为1.然后C语言声明,对于列表中的其余项,当前对象从那里开始递增.因此2到索引[1]处的第二项,依此类推.如果那里有尾随的逗号,没有人关心.

  • 为了处理空初始化器列表的情况,我们可以简单地将整个[0] = __VA_ARGS__技巧放在__VA_OPT__中.这样,如果数组的大小为零,则只添加sentinel.

完成宏:

#define SENTINEL 40
#define COUNT_ARGS(...) ( 0 __VA_OPT__(+ sizeof((int[]){__VA_ARGS__}) / sizeof(int)) )
#define bar(...) { [COUNT_ARGS(__VA_ARGS__)] = SENTINEL, __VA_OPT__([0] = __VA_ARGS__) }

自成一体的测试示例:

#include <stdio.h>

#define SENTINEL 40
#define COUNT_ARGS(...) ( 0 __VA_OPT__(+ sizeof((int[]){__VA_ARGS__}) / sizeof(int)) )
#define bar(...) { [COUNT_ARGS(__VA_ARGS__)] = SENTINEL, __VA_OPT__([0] = __VA_ARGS__) }

#define TEST(arr) for(size_t i=0; i<sizeof(arr)/sizeof*arr; i++) printf("%d ", arr[i]); puts("");
int main (void)
{
  int foo1[] = bar(10, 20, 30);
  int foo2[] = bar(10, 20, 30,);
  int foo3[] = bar();
  int foo4[] = bar(10, 20, 30, 50, 60);

  TEST(foo1);
  TEST(foo2);
  TEST(foo3);
  TEST(foo4);
}

输出:

10 20 30 40 
10 20 30 40 
40 
10 20 30 50 60 40 

回答"__VA_OPT__(,)能检测到后面没有逗号的逗号吗?"不,它不能,但当允许一个空的初始值设定项列表时,这是一个很方便的技巧.这里拖尾逗号的实际修复是指定的初始值设定项.

C++相关问答推荐

如何启用ss(另一个调查套接字的实用程序)来查看Linux主机上加入的多播组IP地址?

单指针和空参数列表之间的函数指针兼容性

C语言中字符数组声明中的标准

文件权限为0666,但即使以超级用户身份也无法打开

CSAPP微型shell 实验室:卡在sigprocmask

如何在ASM中访问C struct 成员

Flose()在Docker容器中抛出段错误

CC2538裸机项目编译但不起作用

如何在STM8项目中导入STM8S/A标准外设库(ST VisualDeveloper)?

有什么方法可以将字符串与我们 Select 的子字符串分开吗?喜欢:SIN(LOG(10))

不同出处的指针可以相等吗?

在libwget中启用Cookie会导致分段故障

运行时错误:在索引数组时加载类型为';char';`的空指针

从C中的函数返回静态字符串是不是一种糟糕的做法?

如何在不更改格式说明符的情况下同时支持双精度和长双精度?

为什么会导致分段故障?(C语言中的一个程序,统计文件中某个单词的出现次数)

当读取可能会阻塞管道中的父进程时,为什么要等待子进程?

传递参数:C 和 C++ 中 array 与 *&array 和 &array[0] 的区别

我怎样才能用c语言正常运行这两个进程?

如何在C中以0x格式打印十六进制值