海湾合作委员会 #__ VA_ARGS__ 诡计的标准替代方案?

对于 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!",);

注意后面的逗号-不可行。

一些编译器(例如: VisualStudio2010)会悄悄地为您去掉尾部的逗号。其他编译器(例如: 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 ()打印出来,如果日志记录是行缓冲的,并且异步地来自多个源,那么这样做是有好处的。

93501 次浏览

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

早在2001年,我就在 文件 N976中编写了 GCC 标准化扩展(以及相关的扩展,它允许您使用 __VA_ARGS__以外的名称作为其余参数) ,但是委员会没有给出任何回应; 我甚至不知道是否有人读过它。2016年,N2023再次提出了这个建议,我鼓励任何知道这个建议的人在评论中告诉我们。

标准的解决方案是使用 FOO而不是 BAR。有一些奇怪的重新排序参数的情况下,它可能不能为你做(虽然我打赌,有人可以拿出聪明的黑客,以反汇编和重新组装 __VA_ARGS__有条件的基础上的参数的数量但一般来说,使用 FOO“通常”只是工作。

这不是一个通用的解决方案,但是在 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。

我最近也遇到了类似的问题,我相信一定有解决的办法。

其核心思想是有一种方法可以编写一个宏 NUM_ARGS来计算给定的可变宏的参数数量。您可以使用 NUM_ARGS的一个变体来构建 NUM_ARGS_CEILING2,它可以告诉您一个可变宏是给定1个参数还是给定2个或更多个参数。然后您可以编写您的 Bar宏,以便它使用 NUM_ARGS_CEILING2CONCAT将其参数发送到两个助手宏中的一个: 一个预期正好有1个参数,另一个预期参数数目大于1的可变数目。

下面是一个例子,我使用这个技巧来编写宏 UNIMPLEMENTED,它非常类似于 BAR:

第一步:

/**
* 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)

第二步:

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

第三步:

#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] ,而不是参数。在这里 在特定的情况下,它实际上对我们的 _ UNIMPLEMENTED1可以很好地处理空标记 它使我们不必写 _ UNIMPLEMENTED0 也可以解决这个问题,虽然我还没用过我不确定它是否适用于我们现在所做的事情。

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

下面是 jwd 提出的问题中实现第二个 BAR()示例的一种符合标准的方法:

#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个或更多的参数,就中断(因为它扩展到第10个参数)。

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

REST_HELPER_ONE()REST_HELPER_TWOORMORE()的选择是通过将 REST_HELPER_REST_HELPER2()NUM(__VA_ARGS__)的扩增连接起来完成的。请注意,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

有一种方法可以处理这种特殊情况下使用类似 启动,预处理器。可以使用 大小检查参数列表的大小,然后有条件地展开到另一个宏。这种方法的一个缺点是它无法区分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

空的宏参数列表实际上包含一个碰巧为空的参数。

在这种情况下,我们很幸运,因为您想要的宏总是至少有一个参数,我们可以将其实现为两个“重载”宏:

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

这是我使用的简化版本。它基于这里其他答案的伟大技巧,这么多的道具给他们:

#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参数。参数名称 count down (而不是 up)提醒您,基于 count 的 SUFFIX参数是以相反的顺序提供的。

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

如果愿意,可以创造性地使用 _SELECT,并为不同数量的参数提供不同的宏。例如,这里我们有一个 LOG 宏,它在格式之前接受一个“ level”参数。如果格式缺失,它会记录“(无消息)”,如果只有一个参数,它会通过“% s”记录,否则它会把格式参数当作其余参数的格式化字符串。

#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
*/

在您的情况下(至少有一个参数,从不为0) ,您可以将 BAR定义为 BAR(...),使用 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编译。

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

#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就满足了。

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

上网试试!

假设:

  • 参数不包含逗号或括号
  • 没有包含 A ~ G的参数(可以重命名为硬碰撞参数)

如果 c + + 11或更高版本是可用的,并且宏打算扩展为一个函数调用,那么您可以为它制作一个包装器,例如:
#define BAR(fmt, ...) printf(fmt, __VA_ARGS__)
可以转换为
#define BAR(fmt, ...) BAR_wrapper(fmt)(__VA_ARGS__)
其中 BAR_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); }

如果您正在使用 gcc 8+clang 6+MSVC 2019(来源) ,那么您也可以使用(较新的) __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 中引入) ,因此您的编译器可能还不支持它。