GCC的标准替代## __ VA_ARGS__技巧?

jwd*_*jwd 141 c c99 c-preprocessor variadic-macros

C99中的可变参数宏存在一个众所周知的 空args 问题.

例:

#define FOO(...)       printf(__VA_ARGS__)
#define BAR(fmt, ...)  printf(fmt, __VA_ARGS__)

FOO("this works fine");
BAR("this breaks!");
Run Code Online (Sandbox Code Playgroud)

BAR()根据C99标准,上述用途确实不正确,因为它将扩展到:

printf("this breaks!",);
Run Code Online (Sandbox Code Playgroud)

请注意尾随逗号 - 不可行.

一些编译器(例如:Visual Studio 2010)将悄然摆脱那个尾随的逗号.其他编译器(例如:GCC)支持放在##前面__VA_ARGS__,如下所示:

#define BAR(fmt, ...)  printf(fmt, ##__VA_ARGS__)
Run Code Online (Sandbox Code Playgroud)

但有没有符合标准的方法来获得这种行为?也许使用多个宏?

现在,该##版本似乎得到了相当好的支持(至少在我的平台上),但我真的更喜欢使用符合标准的解决方案.

先发制人:我知道我可以写一个小功能.我正在尝试使用宏来做到这一点.

编辑:以下是我想要使用BAR()的一个例子(虽然简单):

#define BAR(fmt, ...)  printf(fmt "\n", ##__VA_ARGS__)

BAR("here is a log message");
BAR("here is a log message with a param: %d", 42);
Run Code Online (Sandbox Code Playgroud)

这会自动为我的BAR()日志记录语句添加换行符,假设fmt它始终是双引号C字符串.它不会将换行符打印为单独的printf(),如果日志记录是行缓冲的并且异步来自多个源,则这是有利的.

Ric*_*sen 108

你可以使用一个参数计数技巧.

这是BAR()在jwd的问题中实现第二个例子的一种符合标准的方法:

#include <stdio.h>

#define BAR(...) printf(FIRST(__VA_ARGS__) "\n" REST(__VA_ARGS__))

/* expands to the first argument */
#define FIRST(...) FIRST_HELPER(__VA_ARGS__, throwaway)
#define FIRST_HELPER(first, ...) first

/*
 * if there's only one argument, expands to nothing.  if there is more
 * than one argument, expands to a comma followed by everything but
 * the first argument.  only supports up to 9 arguments but can be
 * trivially expanded.
 */
#define REST(...) REST_HELPER(NUM(__VA_ARGS__), __VA_ARGS__)
#define REST_HELPER(qty, ...) REST_HELPER2(qty, __VA_ARGS__)
#define REST_HELPER2(qty, ...) REST_HELPER_##qty(__VA_ARGS__)
#define REST_HELPER_ONE(first)
#define REST_HELPER_TWOORMORE(first, ...) , __VA_ARGS__
#define NUM(...) \
    SELECT_10TH(__VA_ARGS__, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway)
#define SELECT_10TH(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, ...) a10

int
main(int argc, char *argv[])
{
    BAR("first test");
    BAR("second test: %s", "a string");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

同样的技巧用于:

说明

策略是__VA_ARGS__分成第一个参数和其余参数(如果有的话).这使得在第一个参数之后但在第二个参数之前(如果存在)插入东西成为可能.

FIRST()

这个宏只是扩展到第一个参数,丢弃其余的.

实施很简单.该throwaway参数确保FIRST_HELPER()获得两个参数,这是必需的,因为...需要至少一个.有一个参数,它扩展如下:

  1. FIRST(firstarg)
  2. FIRST_HELPER(firstarg, throwaway)
  3. firstarg

有两个或更多,它扩展如下:

  1. FIRST(firstarg, secondarg, thirdarg)
  2. FIRST_HELPER(firstarg, secondarg, thirdarg, throwaway)
  3. firstarg

REST()

这个宏扩展到除第一个参数之外的所有参数(如果有多个参数,则包括第一个参数之后的逗号).

这个宏的实现要复杂得多.一般策略是计算参数的数量(一个或多个),然后扩展到任一个REST_HELPER_ONE()(如果只给出一个参数)或REST_HELPER_TWOORMORE()(如果给出两个或多个参数). REST_HELPER_ONE()简单地扩展为空 - 在第一个之后没有参数,所以剩下的参数是空集. REST_HELPER_TWOORMORE()也很简单 - 它扩展为逗号,后跟除第一个参数之外的所有内容.

使用NUM()宏计算参数.ONE如果只给出一个参数,TWOORMORE如果给出了两个和九个参数,则该宏扩展为,如果给出10个或更多个参数则断开(因为它扩展到第10个参数).

NUM()宏使用SELECT_10TH()宏来确定参数的个数.顾名思义,SELECT_10TH()只需扩展到第10个参数.由于省略号,SELECT_10TH()需要传递至少11个参数(标准表示省略号必须至少有一个参数).这就是为什么NUM()传递throwaway作为最后一个参数(没有它,传递一个参数NUM()将导致只传递10个参数SELECT_10TH(),这将违反标准).

通过与in 的扩展连接来选择REST_HELPER_ONE()或者REST_HELPER_TWOORMORE()完成.请注意,目的是确保在连接之前完全展开.REST_HELPER_NUM(__VA_ARGS__)REST_HELPER2()REST_HELPER()NUM(__VA_ARGS__)REST_HELPER_

扩展一个论点如下:

  1. REST(firstarg)
  2. REST_HELPER(NUM(firstarg), firstarg)
  3. REST_HELPER2(SELECT_10TH(firstarg, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway), firstarg)
  4. REST_HELPER2(ONE, firstarg)
  5. REST_HELPER_ONE(firstarg)
  6. (空)

有两个或更多参数的扩展如下:

  1. REST(firstarg, secondarg, thirdarg)
  2. REST_HELPER(NUM(firstarg, secondarg, thirdarg), firstarg, secondarg, thirdarg)
  3. REST_HELPER2(SELECT_10TH(firstarg, secondarg, thirdarg, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway), firstarg, secondarg, thirdarg)
  4. REST_HELPER2(TWOORMORE, firstarg, secondarg, thirdarg)
  5. REST_HELPER_TWOORMORE(firstarg, secondarg, thirdarg)
  6. , secondarg, thirdarg

  • @ChrisDodd:对。不幸的是,似乎没有一种方法可以避免不依赖于编译器特定的扩展而限制参数的数量。另外,我还不知道一种可靠地测试是否有太多参数的方法(这样可以打印出有用的编译器错误消息,而不是出现奇怪的故障)。 (2认同)

zwo*_*wol 61

,##__VA_ARGS__如果您愿意接受可以传递给可变参数宏的参数数量的某些硬编码上限,则可以避免使用GCC的扩展名,如Richard Hansen对此问题的回答所述.但是,如果您不希望有任何此类限制,则据我所知,仅使用C99指定的预处理器功能是不可能的; 你必须使用一些语言扩展.clang和icc采用了这个GCC扩展,但MSVC没有.

早在2001年,我__VA_ARGS__文件N976中写了标准化的GCC扩展(以及允许你使用除rest参数之外的名称的相关扩展),但是没有收到委员会的任何回复; 我甚至不知道是否有人读过它.2016年在N2023再次提出,我鼓励任何知道该提案如何在评论中告知我们的人.

  • 此扩展适用于clang&intel icc编译器以及gcc. (5认同)
  • 是的,特别是下半部分.可能有关于`comp.std.c`的讨论,但我现在无法在Google网上找到任何内容; 它当然从来没有得到实际委员会的任何关注(如果确实如此,没有人告诉过我). (4认同)
  • 从我无法在网上找到解决方案以及这里缺乏答案来看,我想你是对的): (2认同)
  • 您指的是[n976](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n976.htm)吗?我搜索了[C工作组](http://www.open-std.org/jtc1/sc22/wg14/)的其余部分[文档](http://www.open-std.org/jtc1 / sc22 / wg14 / www / docs /)进行响应,但未找到响应。甚至没有在[下一次会议的议程]中进行(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n981.htm)。关于此主题的唯一其他热门话题是[n868](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n868.htm)中挪威的第4条评论(再没有后续讨论)。 (2认同)

Mar*_*Ray 16

不是一般解决方案,但在printf的情况下,您可以附加一个换行符,如:

#define BAR_HELPER(fmt, ...) printf(fmt "\n%s", __VA_ARGS__)
#define BAR(...) BAR_HELPER(__VA_ARGS__, "")
Run Code Online (Sandbox Code Playgroud)

我相信它忽略了格式字符串中没有引用的任何额外的args.所以你甚至可以逃脱:

#define BAR_HELPER(fmt, ...) printf(fmt "\n", __VA_ARGS__)
#define BAR(...) BAR_HELPER(__VA_ARGS__, 0)
Run Code Online (Sandbox Code Playgroud)

我不敢相信C99在没有标准方法的情况下获得批准.AFAICT问题也存在于C++ 11中.


DRa*_*ayX 11

有一种方法可以使用Boost.Preprocessor之类的东西来处理这种特定情况.您可以使用BOOST_PP_VARIADIC_SIZE来检查参数列表的大小,然后条件扩展到另一个宏.这样做的一个缺点是,它无法区分0和1参数,一旦您考虑以下因素,其原因就变得清晰了:

BOOST_PP_VARIADIC_SIZE()      // expands to 1
BOOST_PP_VARIADIC_SIZE(,)     // expands to 2
BOOST_PP_VARIADIC_SIZE(,,)    // expands to 3
BOOST_PP_VARIADIC_SIZE(a)     // expands to 1
BOOST_PP_VARIADIC_SIZE(a,)    // expands to 2
BOOST_PP_VARIADIC_SIZE(,b)    // expands to 2
BOOST_PP_VARIADIC_SIZE(a,b)   // expands to 2
BOOST_PP_VARIADIC_SIZE(a, ,c) // expands to 3
Run Code Online (Sandbox Code Playgroud)

空宏参数列表实际上由一个恰好为空的参数组成.

在这种情况下,我们很幸运,因为你想要的宏总是至少有一个参数,我们可以把它实现为两个"重载"宏:

#define BAR_0(fmt) printf(fmt "\n")
#define BAR_1(fmt, ...) printf(fmt "\n", __VA_ARGS__)
Run Code Online (Sandbox Code Playgroud)

然后另一个宏在它们之间切换,例如:

#define BAR(...) \
    BOOST_PP_CAT(BAR_, BOOST_PP_GREATER(
        BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1))(__VA_ARGS__) \
    /**/
Run Code Online (Sandbox Code Playgroud)

要么

#define BAR(...) BOOST_PP_IIF( \
    BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1), \
        BAR_1, BAR_0)(__VA_ARGS__) \
    /**/
Run Code Online (Sandbox Code Playgroud)

无论你发现哪个更具可读性(我更喜欢第一个,因为它为你提供了一个在参数数量上重载宏的一般形式).

通过访问和更改变量参数列表,也可以使用单个宏执行此操作,但它的可读性较低,并且非常特定于此问题:

#define BAR(...) printf( \
    BOOST_PP_VARIADIC_ELEM(0, __VA_ARGS__) "\n" \
    BOOST_PP_COMMA_IF( \
        BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1)) \
    BOOST_PP_ARRAY_ENUM(BOOST_PP_ARRAY_POP_FRONT( \
        BOOST_PP_VARIADIC_TO_ARRAY(__VA_ARGS__)))) \
    /**/
Run Code Online (Sandbox Code Playgroud)

另外,为什么没有BOOST_PP_ARRAY_ENUM_TRAILING?它会使这个解决方案变得不那么可怕.

编辑:好的,这是一个BOOST_PP_ARRAY_ENUM_TRAILING,以及一个使用它的版本(现在这是我最喜欢的解决方案):

#define BOOST_PP_ARRAY_ENUM_TRAILING(array) \
    BOOST_PP_COMMA_IF(BOOST_PP_ARRAY_SIZE(array)) BOOST_PP_ARRAY_ENUM(array) \
    /**/

#define BAR(...) printf( \
    BOOST_PP_VARIADIC_ELEM(0, __VA_ARGS__) "\n" \
    BOOST_PP_ARRAY_ENUM_TRAILING(BOOST_PP_ARRAY_POP_FRONT( \
        BOOST_PP_VARIADIC_TO_ARRAY(__VA_ARGS__)))) \
    /**/
Run Code Online (Sandbox Code Playgroud)


Sim*_*onW 8

我用于调试打印的一个非常简单的宏:

#define DBG__INT(fmt, ...) printf(fmt "%s", __VA_ARGS__);
#define DBG(...) DBG__INT(__VA_ARGS__, "\n")

int main() {
        DBG("No warning here");
        DBG("and we can add as many arguments as needed. %s", "nice!");
        return 0;
}
Run Code Online (Sandbox Code Playgroud)

无论有多少参数传递给 DBG,都没有 c99 警告。

诀窍是DBG__INT添加一个虚拟参数,因此...将始终至少有一个参数并且满足 c99。


Use*_*abc 5

最近,我遇到了类似的问题,而且我确实相信有解决方案。

关键思想是,有一种方法可以编写一个宏NUM_ARGS来计算可变参数宏给出的参数数量。您可以使用NUM_ARGSbuild 的变体,该变量NUM_ARGS_CEILING2可以告诉您可变参数宏是被赋予1个参数还是2个或更多个参数。然后,你可以写你的Bar,以便它使用宏NUM_ARGS_CEILING2CONCAT其中一个期望的是1周的说法,而另一个预期的参数个数可变大于1:到它的参数发送给两个辅助宏之一。

这是我使用此技巧编写宏的示例,该宏UNIMPLEMENTED非常类似于BAR

步骤1:

/** 
 * A variadic macro which counts the number of arguments which it is
 * passed. Or, more precisely, it counts the number of commas which it is
 * passed, plus one.
 *
 * Danger: It can't count higher than 20. If it's given 0 arguments, then it
 * will evaluate to 1, rather than to 0.
 */

#define NUM_ARGS(...)                                                   \
    NUM_ARGS_COUNTER(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13,       \
                     12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)    

#define NUM_ARGS_COUNTER(a1, a2, a3, a4, a5, a6, a7,        \
                         a8, a9, a10, a11, a12, a13,        \
                         a14, a15, a16, a17, a18, a19, a20, \
                         N, ...)                            \
    N
Run Code Online (Sandbox Code Playgroud)

步骤1.5:

/*
 * A variant of NUM_ARGS that evaluates to 1 if given 1 or 0 args, or
 * evaluates to 2 if given more than 1 arg. Behavior is nasty and undefined if
 * it's given more than 20 args.
 */

#define NUM_ARGS_CEIL2(...)                                           \
    NUM_ARGS_COUNTER(__VA_ARGS__, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
                     2, 2, 2, 2, 2, 2, 2, 1)
Run Code Online (Sandbox Code Playgroud)

第2步:

#define _UNIMPLEMENTED1(msg)                                        \
    log("My creator has forsaken me. %s:%s:%d." msg, __FILE__,      \
        __func__, __LINE__)

#define _UNIMPLEMENTED2(msg, ...)                                   \
    log("My creator has forsaken me. %s:%s:%d." msg, __FILE__,      \
        __func__, __LINE__, __VA_ARGS__)
Run Code Online (Sandbox Code Playgroud)

步骤3:

#define UNIMPLEMENTED(...)                                              \
    CONCAT(_UNIMPLEMENTED, NUM_ARGS_CEIL2(__VA_ARGS__))(__VA_ARGS__)
Run Code Online (Sandbox Code Playgroud)

以常规方式实施CONCAT的位置。作为快速提示,如果以上内容令人困惑:CONCAT的目标是扩展到另一个宏“调用”。

请注意,未使用NUM_ARGS本身。我只是将其包括在内,以说明此处的基本技巧。请参阅Jens Gustedt的P99博客,以获取对它的很好处理。

两个注意事项:

  • NUM_ARGS处理的参数数量受到限制。我的最多只能处理20个,尽管数量是任意的。

  • 如图所示,NUM_ARGS有一个陷阱,即当给定0个参数时它返回1。其要点是NUM_ARGS技术上是在计算[逗号+ 1],而不是args。在这种情况下,它实际上对我们有利。_UNIMPLEMENTED1可以很好地处理一个空令牌,这使我们不必编写_UNIMPLEMENTED0。Gustedt也有一个解决方法,尽管我没有使用它,而且我不确定它是否适合我们在此所做的工作。

  • 我永远也不会想到这种方法,因此我写了大约GCC当前预处理器的一半!就是说,我仍然要说“没有标准的方法可以达到这种效果”,因为您和Richard的技术都对宏参数的数量施加了上限。 (2认同)