如何使预处理宏具备贪婪性?

4
我们有以下预处理宏。它用于帮助 Doxygen 文档编写,因为 Doxygen 在 C++ 和一些模板 typedef 上存在问题:
#if defined(DOXYGEN_PROCESSING)
# define DOCUMENTED_TYPEDEF(x, y) class y : public x {};
#else
# define DOCUMENTED_TYPEDEF(x, y) typedef x y;
#endif

X 是非模板或只有一个模板参数时,它表现得非常好。然而,如果 X 是具有多个参数的模板:

DOCUMENTED_TYPEDEF(Foo<R,S>,Bar);

当字符串被分成Foo<RS>,Bar时(并且它没有生成文档),会导致编译错误。

如何使预处理器宏贪婪?

2个回答

5

您可能不会喜欢这个消息:

#define COMMA ,

#if defined(DOXYGEN_PROCESSING)
# define DOCUMENTED_TYPEDEF(x, y) class y : public x {};
#else
# define DOCUMENTED_TYPEDEF(x, y) typedef x y;
#endif

DOCUMENTED_TYPEDEF(Foo<R COMMA S>,Bar)

测试:

$ gcc -E comma-macro.c 
# 1 "comma-macro.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "comma-macro.c"
# 9 "comma-macro.c"
typedef Foo<R , S> Bar;

在任何替换发生之前,宏参数列表都会先进行括号和逗号的解析。然后再将 x 参数中的 COMMA 替换,并将 x 替换到宏体中。此时,参数已经断开;COMMA 被替换为逗号标点符号不再具有相关性。不过,该逗号将会分隔由该宏生成的任何宏调用中出现的参数,因此如果这些参数需要受到保护,则需要使用更多的技巧。

你可以使用函数宏来隐藏 COMMA,例如使用 PAIR

#define COMMA ,

#define PAIR(A, B) A COMMA B

#if defined(DOXYGEN_PROCESSING)
# define DOCUMENTED_TYPEDEF(x, y) class y : public x {};
#else
# define DOCUMENTED_TYPEDEF(x, y) typedef x y;
#endif

DOCUMENTED_TYPEDEF(PAIR(Foo<R, S>), Bar)

乍一看更有吸引力,但可能存在缺点。它更加模糊。读者会想知道PAIR后面是否有语义?而COMMA看起来过于晦涩难懂,对于那些与预处理器斗争过的人来说,其目的很可能是显而易见的。
关于PAIR,我们可以隐藏它,并最终得到类似于Zwol答案中的语法。但是,我们需要多个DOCUMENTED_TYPEDEF的变体。
另外,顺便说一下,让我们放弃无用的COMMA,因为在宏的右侧不需要它。
#define PAIR(A, B) A, B

#if defined(DOXYGEN_PROCESSING)
# define DOCUMENTED_TYPEDEF_2(x2, y) class y : public PAIR x2 {};
#else
# define DOCUMENTED_TYPEDEF_2(x2, y) typedef PAIR x2 y;
#endif

DOCUMENTED_TYPEDEF_2((<R, S>), Bar)
$ gcc -std=c90 -E -Wall -pedantic comma-macro.c 
# 1 "comma-macro.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "comma-macro.c"
# 11 "comma-macro.c"
typedef <R, S> Bar;

这看起来可能可以使用 C99 风格的可变宏来完成。然而,这可能会违反评论中讨论的可移植性要求,更不用说这是 C++ 了。为了将来的读者,请注意:

#define PNEUMATIC_COMMA_GUN(A, ...) A, ## __VA_ARGS__

#if defined(DOXYGEN_PROCESSING)
# define DOCUMENTED_TYPEDEF(xv, y) class y : public PNEUMATIC_COMMA_GUN xv {};
#else
# define DOCUMENTED_TYPEDEF(xv, y) typedef PNEUMATIC_COMMA_GUN xv y;
#endif

DOCUMENTED_TYPEDEF((<R, S, T, L, N, E>), Bar)
$ gcc -std=c99 -E -Wall -pedantic comma-macro.c  
# 1 "comma-macro.c"
# 1 "<内置>"
# 1 "<命令行>"
# 1 "comma-macro.c"
# 9 "comma-macro.c"
定义了一个变量Bar,类型为<R, S, T, L, N, E>,涉及到的技术为it。

那很丑。我总是可以依赖一个非贪婪匹配吗?如果可以的话,我打算颠倒 XY。棘手的问题似乎是:什么是明确定义的,而什么是实现定义的。我需要它在几乎所有平台上的编译器上都能工作,并回退到 1990 年代。这是一种特殊的地狱。 - jww
预处理器会在未被括号包裹的逗号处分割宏参数列表。(它会计算圆括号标记以使其平衡)。方括号、花括号或尖括号不会被识别;它们对逗号没有保护作用。这与贪婪程度无关。 - Kaz
关于可移植性,我只使用了C90预处理器语法。 - Kaz

3

无法更改预处理器解析宏参数的方式。没有括号包含的逗号总是分隔宏参数。

您可能能够做的是:

DOCUMENTED_TYPEDEF((Foo<R,S>), Bar);

当然,只有当宏的扩展中出现内部括号没有问题时,这种方法才有效。我不记得这是否会在您展示的上下文中造成问题。

如果可以要求C99可变参数宏,则可以使用它们来消除额外的括号:

#define STRIP_PARENS(...) __VA_ARGS__
#if defined(DOXYGEN_PROCESSING)
# define DOCUMENTED_TYPEDEF(x, y) class y : public STRIP_PARENS x {};
#else
# define DOCUMENTED_TYPEDEF(x, y) typedef STRIP_PARENS x y;
#endif

DOCUMENTED_TYPEDEF((Foo<R,S>), Bar);

但现在你需要在DOCUMENTED_TYPEDEF的第一个参数周围始终添加额外的一对括号。


我怀疑这会引起问题,因为左侧参数X被放置在声明说明符的位置。在C和C++风格的声明中,声明说明符不带有可选的括号。然而,声明符却具备这个特性:int x;int (x);是相同的。(int) x;看起来像一个类型转换表达式。 - Kaz
@Kaz,你能接受在可能包含模板参数的参数周围始终加上括号,并且只支持实现C99可变宏的编译器吗?如果可以,有一个技巧可以解决。 - zwol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接