标准替代GCC的## __ VA_ARGS__技巧?
在C99中,可变macros的空参数有一个众所周知的 问题 。
例:
#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__)
但是有没有符合标准的方法来获得这种行为? 也许使用多个macros?
现在, ##
版本似乎得到了很好的支持(至less在我的平台上),但是我更愿意使用符合标准的解决scheme。
先发制人:我知道我只能写一个小function。 我正在尝试使用macros来做到这一点。
编辑 :这是一个例子(虽然简单)为什么我想要使用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
总是一个双引号的Cstring。 它不会将换行符作为单独的printf()进行打印,如果日志logging是行caching的并且是asynchronous来自多个源的,则这是有利的。
如Richard Hansen对这个问题的回答所描述的,如果你愿意接受一些你可以传递给可变参数macros的参数的硬编码上限,那么可以避免使用GCC的扩展名,##__VA_ARGS__
。 如果你不想有任何这样的限制,但是据我所知,仅仅使用C99指定的预处理器特性是不可能的。 你必须使用一些扩展的语言。 铿锵和icc采用了这个GCC扩展,但MSVC没有。
早在2001年,我就在__VA_ARGS__
文档中写下了GCC扩展标准化(以及相关的扩展名,可以使用不同于__VA_ARGS__
的名称作为其他参数),但没有收到委员会的任何回复。 我甚至不知道有没有人读过它。 在2016年, N2023再次提出了这个build议,我鼓励任何知道这个build议将如何让我们知道的人。
有一个参数计数技巧,你可以使用。
下面是在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__
说明
策略是将__VA_ARGS__
分隔成第一个参数和其余部分(如果有的话)。 这使得可以在第一个参数之后但在第二个之前(如果存在)插入东西。
FIRST()
这个macros只是扩展到第一个参数,其余的都是抛弃。
实施很简单。 throwaway
参数确保FIRST_HELPER()
获取两个参数,这是必需的,因为...
需要至less一个参数。 有一个论点,它扩展如下:
-
FIRST(firstarg)
-
FIRST_HELPER(firstarg, throwaway)
-
firstarg
有两个或更多,它扩展如下:
-
FIRST(firstarg, secondarg, thirdarg)
-
FIRST_HELPER(firstarg, secondarg, thirdarg, throwaway)
-
firstarg
REST()
这个macros扩展到除了第一个参数(包括第一个参数之后的逗号,如果有多个参数)之外的所有东西。
这个macros的实现要复杂得多。 一般策略是计算参数个数(一个或多个参数),然后展开为REST_HELPER_ONE()
(如果只给出一个参数)或REST_HELPER_TWOORMORE()
(如果给出两个或多个参数)。 REST_HELPER_ONE()
简单地扩展为空 – 第一个之后没有参数,所以剩下的参数是空集。 REST_HELPER_TWOORMORE()
也很简单 – 它扩展为一个逗号,除了第一个参数外,其余都是逗号。
参数使用NUM()
macros进行计数。 如果只给出一个参数,则该macros扩展为ONE
如果给出两个和九个参数之间的TWOORMORE
,并且如果给出10个或更多个参数(因为它扩展到第十个参数)则中断。
NUM()
macros使用SELECT_10TH()
macros来确定参数的数量。 顾名思义, SELECT_10TH()
简单地扩展到第10个参数。 由于省略号, SELECT_10TH()
需要传递至less11个参数(标准说至less有一个省略号参数)。 这就是为什么NUM()
作为最后一个parameter passing一次(没有它,传递一个参数到NUM()
将导致只有10个parameter passing给SELECT_10TH()
,这将违反标准)。
REST_HELPER_ONE()
或REST_HELPER_TWOORMORE()
是通过连接REST_HELPER_
和REST_HELPER_TWOORMORE()
中的NUM(__VA_ARGS__)
的扩展来完成的。 请注意, REST_HELPER()
的目的是确保NUM(__VA_ARGS__)
在与REST_HELPER_
连接之前完全展开。
一个论点的扩展如下:
-
REST(firstarg)
-
REST_HELPER(NUM(firstarg), firstarg)
-
REST_HELPER2(SELECT_10TH(firstarg, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway), firstarg)
-
REST_HELPER2(ONE, firstarg)
-
REST_HELPER_ONE(firstarg)
- (空)
有两个或更多参数的扩展如下:
-
REST(firstarg, secondarg, thirdarg)
-
REST_HELPER(NUM(firstarg, secondarg, thirdarg), firstarg, secondarg, thirdarg)
-
REST_HELPER2(SELECT_10TH(firstarg, secondarg, thirdarg, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway), firstarg, secondarg, thirdarg)
-
REST_HELPER2(TWOORMORE, firstarg, secondarg, thirdarg)
-
REST_HELPER_TWOORMORE(firstarg, secondarg, thirdarg)
-
, secondarg, thirdarg
不是一个通用的解决scheme,但在printf的情况下,你可以添加一个换行符,如:
#define BAR_HELPER(fmt, ...) printf(fmt "\n%s", __VA_ARGS__) #define BAR(...) BAR_HELPER(__VA_ARGS__, "")
我相信它会忽略任何格式string中未引用的额外参数。 所以你甚至可以逃避:
#define BAR_HELPER(fmt, ...) printf(fmt "\n", __VA_ARGS__) #define BAR(...) BAR_HELPER(__VA_ARGS__, 0)
我不能相信C99被批准没有一个标准的方式来做到这一点。 AFAICT也存在C ++ 11中的问题。
有一种方法可以使用Boost.Preprocessor来处理这个特定的情况。 您可以使用BOOST_PP_VARIADIC_SIZE来检查参数列表的大小,然后有条件地扩展到另一个macros。 这样做的一个缺点是它不能区分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
空的macros参数列表实际上由一个恰好为空的参数组成。
在这种情况下,我们很幸运,因为你所期望的macros至less有一个参数,我们可以把它作为两个“过载”macros来实现:
#define BAR_0(fmt) printf(fmt "\n") #define BAR_1(fmt, ...) printf(fmt "\n", __VA_ARGS__)
然后是另一个macros在它们之间切换,比如:
#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__) \ /**/
无论你发现更可读(我更喜欢第一,因为它给你一个通用的forms来重载macros参数的数量)。
也可以通过访问和variablesvariables参数列表来完成这个工作,但是它的可读性较差,而且这个问题非常具体:
#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? 这将使这个解决scheme更不可怕。
编辑:好的,这里是一个BOOST_PP_ARRAY_ENUM_TRAILING,和一个使用它的版本(这是我现在最喜欢的解决scheme):
#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__)))) \ /**/
最近我碰到类似的问题,我相信有一个解决scheme。
关键的想法是,有一种方法可以编写一个macrosNUM_ARGS
来计算可变macros的参数个数。 您可以使用NUM_ARGS
的变体来构buildNUM_ARGS_CEILING2
,它可以告诉您可变macros是否被赋予1个参数或2个或更多个参数。 然后你可以编写你的Bar
macros,以便它使用NUM_ARGS_CEILING2
和CONCAT
将它的参数发送给两个助手macros之一:一个需要一个参数,另一个需要大于1的可变参数。
下面是一个例子,我用这个技巧来写macrosUNIMPLEMENTED
,它和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的目标就是扩展到另一个macros“呼叫”。
请注意,NUM_ARGS本身不被使用。 我只是把它包括在这里来说明这里的基本技巧。 请参阅Jens Gustedt的P99博客 ,对其进行很好的处理。
两个注释:
-
NUM_ARGS在处理的参数数量上受到限制。 矿井只能处理20个,尽pipe数量完全是任意的。
-
如图所示,NUM_ARGS有一个缺陷,即在给出0个参数时返回1。 其要点在于,NUM_ARGS在技术上计数[逗号+ 1],而不是参数。 在这个特殊情况下,它实际上对我们有利。 _UNIMPLEMENTED1将处理一个空的标记就好了,它使我们不必编写_UNIMPLEMENTED0。 Gustedt也有一个解决方法,虽然我没有使用它,我不知道这是否会为我们在这里做的工作。
标准的解决scheme是使用FOO
而不是BAR
。 有一些很奇怪的情况,它可能对你没有帮助(虽然我敢打赌,有人可以用巧妙的方式来拆卸和重新组装__VA_ARGS__
,但是一般情况下使用FOO
“通常“只是工作。