158

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

例子:

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

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

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

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

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

4

12 回答 12

116

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

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如果只给出了一个参数,如果给出了 2 到 9 个参数,则此宏扩展为,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
于 2012-06-23T20:19:19.770 回答
69

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

早在 2001 年,我在文档 N976 中编写了用于标准化的 GCC 扩展(以及允许您使用其他名称以外的名称的相关扩展__VA_ARGS__N976,但委员会没有任何回应;我什至不知道有没有人读过。2016 年,它在N2023再次提出,我鼓励任何知道该提案将如何在评论中告诉我们的人。

于 2011-04-08T01:08:09.800 回答
18

不是一般的解决方案,但在 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 中。

于 2011-12-29T21:48:19.473 回答
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__)))) \
    /**/
于 2013-04-19T19:09:32.420 回答
9

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

#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。

于 2018-12-20T19:38:31.183 回答
5

我最近遇到了类似的问题,我相信有一个解决方案。

关键思想是有一种方法可以编写一个宏NUM_ARGS来计算给定可变参数宏的参数数量。您可以使用NUM_ARGSto build的变体NUM_ARGS_CEILING2,它可以告诉您是否为可变参数宏提供了 1 个参数或 2 个或更多参数。然后,您可以编写Bar宏,以便它使用NUM_ARGS_CEILING2并将CONCAT其参数发送到两个辅助宏之一:一个需要正好 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 在技术上计数 [commas + 1],而不是 args。在这种特殊情况下,它实际上对我们有利。_UNIMPLEMENTED1 可以很好地处理空令牌,它使我们不必编写 _UNIMPLEMENTED0。Gustedt 也有一个解决方法,虽然我没有使用它,我不确定它是否适用于我们在这里所做的事情。

于 2012-06-22T19:25:27.193 回答
3

这是我使用的简化版本。它基于此处其他答案的出色技术,对它们有很多支持:

#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 宏,它在格式之前接受一个“级别”参数。如果缺少格式,它会记录“(无消息)”,如果只有 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
*/
于 2018-03-18T15:56:19.007 回答
2

在您的情况下(至少存在 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没有警告。

于 2018-11-11T11:56:34.327 回答
2

如果 c++11 或更高版本可用,并且宏旨在扩展为函数调用,则可以为其制作一个包装器,例如:
#define BAR(fmt, ...) printf(fmt, __VA_ARGS__)
可以转换为
#define BAR(fmt, ...) BAR_wrapper(fmt)(__VA_ARGS__)
whereBAR_wrapper可以定义为:

struct BAR_wrapper_t {
  BAR_wrapper_t(const char* fmt) : fmt(fmt) {}
  const char* fmt;
  int operator()() const { return printf(fmt); }
  template <typename... Args>
  int operator()(Args&& args) const { return printf(fmt, std::forward<Args>(args)...); }
};
inline BAR_wrapper_t BAR_wrapper(const char* fmt) { return BAR_wrapper_t(fmt); }
于 2021-04-02T07:04:13.623 回答
2

如果您正在使用gcc 8+,clang 6+MSVC 2019( source ),那么您还可以使用 (newer)__VA_OPT__宏,如果__VA_ARGS__非空,它有条件地扩展。

因此,我们可以将两者FOOBAR宏合二为一:

#define FOO(s, ...) printf(s __VA_OPT__(,) __VA_ARGS__)

因此,FOO("hello!")将扩展为printf("hello!")FOO("x = %d", 5)并将扩展为printf("x = %d", 5)

这是一个相对较新的功能(在 C++2a 中引入),因此您的编译器可能还不支持它。

于 2021-12-29T06:26:33.223 回答
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 包含A~ G(可以重命名为 hard_collide )
于 2019-03-16T19:55:23.557 回答
-2

标准解决方案是使用FOO而不是BAR. 有一些奇怪的参数重新排序案例可能无法为您做(尽管我敢打赌,有人可以__VA_ARGS__根据其中的参数数量有条件地反汇编和重新组装!)但通常使用FOO“通常”只是工作。

于 2011-04-08T01:54:20.760 回答