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


149

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

例:

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

FOO("this works fine");
BAR("this breaks!");

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

printf("this breaks!",);

请注意结尾的逗号-不可行。

一些编译器(例如:Visual Studio 2010)将悄悄地为您消除尾随的逗号。其他编译器(例如:GCC)也支持放在##前面__VA_ARGS__,如下所示:

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

但是,是否存在符合标准的方法来实现此行为?也许使用多个宏?

现在,该##版本似乎受到了很好的支持(至少在我的平台上),但是我确实希望使用符合标准的解决方案。

先发制人:我知道我可以编写一个小函数。我正在尝试使用宏进行此操作。

编辑:这是为什么我想使用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);

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


3
为什么要使用BAR而不是FOO首先使用?
GManNickG 2011年

@GMan:我在最后添加了一个示例
jwd 2011年

5
@GMan:请读最后一句话(:
jwd 2011年

7
已建议将此功能包含在C2x中。
Leushenko

2
@zwol提交给WG14的最新版本如下所示,它使用基于__VA_OPT__关键字的新语法。这已经被 C ++ “采用”了,所以我希望C会效仿。(不知道这是否意味着它已快速进入C ++ 17或是否已设置为C ++ 20)
Leushenko

Answers:


66

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

早在2001年,我__VA_ARGS__N976号文件中写了GCC标准化扩展(以及相关扩展,使您可以使用其余参数以外的名称),但未得到委员会的任何回应;我什至不知道是否有人读过它。在2016年,N2023再次提出了该建议,我鼓励知道该建议的人在评论中告诉我们。


2
从我在网上找不到解决方案的能力以及此处缺少答案的角度来看,我猜您是对的):
jwd 2011年

2
您所指的是n976吗?我在C工作组的其他文档中进行了搜索,但没有找到答案。甚至以后会议议程都没有。关于这一主题的其他唯一问题是挪威批准了C99批准之前在n868中的第4条评论(再次没有进行后续讨论)。
理查德·汉森

4
是的,特别是第二部分。可能在讨论中,comp.std.c但是我现在在Google网上论坛中找不到任何内容;当然,它从来没有得到实际委员会的关注(或者,如果这样做,没有人告诉过我)。
zwol 2012年

1
恐怕我没有证据,我也不是再考虑的合适人选。我确实写了一半的GCC预处理程序,但是那是十多年前的事了,即使那时我也从未想到过下面的参数计数技巧。
zwol 2014年

6
此扩展程序适用于clang和intel icc编译器以及gcc。
ACyclic

112

您可以使用一个参数计数技巧。

这是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;
}

相同的技巧用于:

说明

该策略是__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个或更多参数,则该宏中断(因为它扩展为第十个参数)。

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

选择REST_HELPER_ONE()REST_HELPER_TWOORMORE()通过REST_HELPER_NUM(__VA_ARGS__)in 的扩展连接来完成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

1
请注意,如果调用带有10个或更多参数的BAR会失败,尽管扩展到更多参数相对容易,但它始终可以处理的参数数量有一个上限
Chris Dodd

2
@ChrisDodd:正确。不幸的是,似乎没有一种方法可以避免不依赖于编译器特定的扩展而限制参数的数量。另外,我还不知道一种可靠地测试是否有太多参数的方法(这样可以打印出有用的编译器错误消息,而不是奇怪的失败)。
理查德·汉森

17

这不是一个通用的解决方案,但是对于printf,您可以添加一个换行符,例如:

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

我相信它会忽略格式字符串中未引用的任何其他参数。因此,您甚至可以摆脱:

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

我不敢相信没有标准方法即可批准C99。AFAICT该问题在C ++ 11中也存在。


这个额外的0的问题是,如果它调用vararg函数,它将实际上最终出现在代码中。检查由理查德·汉森(Richard Hansen)提供的解决方案
Pavel P

@Pavel对于第二个示例是正确的,但第一个示例的效果很好。+1。
kirbyfan64sos 2014年

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

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

在这种情况下,我们很幸运,因为您所需的宏始终至少具有1个参数,因此我们可以将其实现为两个“过载”宏:

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

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

#define BAR(...) \
    BOOST_PP_CAT(BAR_, BOOST_PP_GREATER(
        BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1))(__VA_ARGS__) \
    /**/

要么

#define BAR(...) BOOST_PP_IIF( \
    BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1), \
        BAR_1, BAR_0)(__VA_ARGS__) \
    /**/

无论您觉得可读性如何(我更喜欢第一个,因为它为您提供了一种在参数数量上重载宏的通用形式)。

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

#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__)))) \
    /**/

另外,为什么没有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__)))) \
    /**/

1
很高兴了解Boost.Preprocessor +1。请注意,它BOOST_PP_VARIADIC_SIZE()使用了我在答案中记录的相同的参数计数技巧,并且具有相同的限制(如果传递的参数数目超过一定数量,它将被打破)。
理查德·汉森

1
是的,我看到您的方法与Boost所使用的方法相同,但是boost解决方案维护得很好,并且在开发更复杂的宏时可以使用许多其他真正有用的功能。递归的东西特别酷(并且在最后一个使用BOOST_PP_ARRAY_ENUM的方法中在后台使用)。
DRayX

1
实际上适用于c标签的Boost答案!万岁!
贾斯汀

6

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

#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;
}

无论有多少参数传递给DBG,都不会出现c99警告。

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


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

步骤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)

第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__)

步骤3:

#define UNIMPLEMENTED(...)                                              \
    CONCAT(_UNIMPLEMENTED, NUM_ARGS_CEIL2(__VA_ARGS__))(__VA_ARGS__)

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

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

两个注意事项:

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

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


+1表示提出论点计数技巧,-1表示确实很难遵循
Richard Hansen 2012年

您添加的注释是一种改进,但是仍然存在许多问题:1.您讨论并定义NUM_ARGS但不使用它。2.目的是UNIMPLEMENTED什么?3.您永远不会解决问题中的示例问题。4.一次逐步执行扩展将说明其工作原理,并解释每个帮助程序宏的作用。5.讨论0个论点会分散注意力;OP询问有关标准合规性的信息,并且禁止使用0个参数(C99 6.10.3p4)。6.步骤1.5?为什么不进行第2步?7.“步骤”指顺序发生的动作;这只是代码。
理查德·汉森

8.您链接到整个博客,而不是相关文章。我找不到您指的帖子。9.最后一段是尴尬:这种方法不明显; 这就是为什么没有其他人之前发布过正确的解决方案的原因。同样,如果它工作并遵守标准,则Zack的答案肯定是错误的。10.您应该定义CONCAT()-不要假设读者知道它是如何工作的。
理查德·汉森

(请不要将此反馈理解为攻击-我确实想对您的答案进行投票,但这样做不舒服,除非使之易于理解。如果您可以提高答案的清晰度,我会支持您并删除我的。)
理查德·汉森

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

2

这是我使用的简化版本。它基于此处其他答案的出色技巧,为他们提供了许多支持:

#define _SELECT(PREFIX,_5,_4,_3,_2,_1,SUFFIX,...) PREFIX ## _ ## SUFFIX

#define _BAR_1(fmt)      printf(fmt "\n")
#define _BAR_N(fmt, ...) printf(fmt "\n", __VA_ARGS__);
#define BAR(...) _SELECT(_BAR,__VA_ARGS__,N,N,N,N,1)(__VA_ARGS__)

int main(int argc, char *argv[]) {
    BAR("here is a log message");
    BAR("here is a log message with a param: %d", 42);
    return 0;
}

而已。

与其他解决方案一样,这仅限于宏参数的数量。要支持更多功能,请向添加更多参数_SELECT,并添加更多参数N。参数名称递减计数(而不是递增计数),以提醒基于计数的SUFFIX参数以相反的顺序提供。

此解决方案将0个参数视为1个参数。因此BAR()名义上是“有效的”,因为它扩展为_SELECT(_BAR,,N,N,N,N,1)(),扩展为_BAR_1()(),扩展为printf("\n")

如果需要,您可以通过使用来发挥创意,_SELECT并为不同数量的参数提供不同的宏。例如,这里我们有一个LOG宏,该宏在格式之前带有“ level”参数。如果缺少格式,它将记录“(无消息)”,如果只有1个参数,它将通过“%s”记录它,否则它将把格式参数作为其余参数的printf格式字符串。

#define _LOG_1(lvl)          printf("[%s] (no message)\n", #lvl)
#define _LOG_2(lvl,fmt)      printf("[%s] %s\n", #lvl, fmt)
#define _LOG_N(lvl,fmt, ...) printf("[%s] " fmt "\n", #lvl, __VA_ARGS__)
#define LOG(...) _SELECT(_LOG,__VA_ARGS__,N,N,N,2,1)(__VA_ARGS__)

int main(int argc, char *argv[]) {
    LOG(INFO);
    LOG(DEBUG, "here is a log message");
    LOG(WARN, "here is a log message with param: %d", 42);
    return 0;
}
/* outputs:
[INFO] (no message)
[DEBUG] here is a log message
[WARN] here is a log message with param: 42
*/

使用-pedantic编译时,这仍会触发警告。
PSkocik '18

0

在您的情况下(至少存在1个参数,永不为0),可以将定义BARBAR(...),使用Jens Gustedt HAS_COMMA(...)来检测逗号,然后分派给BAR0(Fmt)或进行BAR1(Fmt,...)相应处理。

这个:

#define HAS_COMMA(...) HAS_COMMA_16__(__VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0)
#define HAS_COMMA_16__(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, ...) _15
#define CAT_(X,Y) X##Y
#define CAT(X,Y) CAT_(X,Y)
#define BAR(.../*All*/) CAT(BAR,HAS_COMMA(__VA_ARGS__))(__VA_ARGS__)
#define BAR0(X) printf(X "\n")
#define BAR1(X,...) printf(X "\n",__VA_ARGS__)


#include <stdio.h>
int main()
{
    BAR("here is a log message");
    BAR("here is a log message with a param: %d", 42);
}

编译时-pedantic没有警告。


0

C(gcc),762字节

#define EMPTYFIRST(x,...) A x (B)
#define A(x) x()
#define B() ,

#define EMPTY(...) C(EMPTYFIRST(__VA_ARGS__) SINGLE(__VA_ARGS__))
#define C(...) D(__VA_ARGS__)
#define D(x,...) __VA_ARGS__

#define SINGLE(...) E(__VA_ARGS__, B)
#define E(x,y,...) C(y(),)

#define NONEMPTY(...) F(EMPTY(__VA_ARGS__) D, B)
#define F(...) G(__VA_ARGS__)
#define G(x,y,...) y()

#define STRINGIFY(...) STRINGIFY2(__VA_ARGS__)
#define STRINGIFY2(...) #__VA_ARGS__

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

int main() {
    puts(STRINGIFY(NONEMPTY()));
    puts(STRINGIFY(NONEMPTY(1)));
    puts(STRINGIFY(NONEMPTY(,2)));
    puts(STRINGIFY(NONEMPTY(1,2)));

    BAR("here is a log message");
    BAR("here is a log message with a param: %d", 42);
}

在线尝试!

假设:

  • arg不包含逗号或括号
  • 没有arg包含AG(可以重命名为hard_collide的)

no arg contain comma可以通过在更多遍之后检查多次来绕过该限制,但是no bracket仍然存在
l4m2

-2

标准解决方案是使用FOO而不是BAR。有几种怪异的参数重排方式可能对您不可行(尽管我敢打赌,有人可以__VA_ARGS__根据其中的参数数量来巧妙地破解并有条件地重新组装!),但通常使用FOO“通常”正常工作。


1
问题是“是否存在符合标准的方法来实现这种行为?”
马什·雷

2
问题包括现在不使用FOO的理由。
PavelŠimerda2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.