检查构造常量是否为#define'

Check if a constructed constant is #define'd

我正在尝试构建一个测试来检查某个文件是否定义了具有某个命名空间的 header 守卫。由于测试是通用的,因此此命名空间仅在 compile-time 处已知并作为 -DTHENAMESPACE=BLA 传入。然后我们使用 的一些魔法将其粘贴在一起。

这意味着我想做类似的事情:

#define PASTER(x, y) x##_##y
#define EVALUATOR(x, y) PASTER(x, y)
#define NAMESPACE(fun) EVALUATOR(THENAMESPACE, fun)
#ifndef NAMESPACE(API_H)  // evaluates to BLA_API_H
#  error "namespace not properly defined"
#endif

但这不能正常工作,cpp 抱怨 ifndef 没有期望括号。

如果可能的话,我怎样才能正确地做到这一点?

我也尝试过添加更多的间接层,但没有取得很大的成功。


所以直接正确地执行 #ifdef 这至少看起来是不可能的:

考虑 defined 运算符:

If the defined operator appears as a result of a macro expansion, the C standard says the behavior is undefined. GNU cpp treats it as a genuine defined operator and evaluates it normally. It will warn wherever your code uses this feature if you use the command-line option -Wpedantic, since other compilers may handle it differently. The warning is also enabled by -Wextra, and can also be enabled individually with -Wexpansion-to-defined.

https://gcc.gnu.org/onlinedocs/cpp/Defined.html#Defined

ifdef期待宏,不做进一步扩展。

https://gcc.gnu.org/onlinedocs/cpp/Ifdef.html#Ifdef

但也许可以触发 'undefined constant' 警告 (-Wundef),这也会让我的测试管道发现这个问题。

我认为 #ifndef 指令内的宏扩展在标准 C 领域是不可能的。

#ifndef symbol 等价于 #if !defined symbol。标准是这样说的(§6.10.1 有条件的包含):

  1. ... it may contain unary operator expressions of the form

    defined identifier
    

    or

    defined ( identifier )
    

  1. Prior to evaluation, macro invocations in the list of preprocessing tokens that will become the controlling constant expression are replaced (except for those macro names modified by the defined unary operator), just as in normal text. If the token defined is generated as a result of this replacement process or use of the defined unary operator does not match one of the two specified forms prior to macro replacement, the behavior is undefined. ...

所以基本上 defined 表达式中的标识符不会展开,您当前的 NAMESPACE(API_H) 不是有效的标识符形式。

可能的解决方法是简单地使用:

#if NAMESPACE(API_H) == 0
#  error "namespace not properly defined"
#endif

这是有效的,因为不存在的标识符被替换为 0。这种方法的问题是,如果BLA_API_H 定义为0,将会出现假错误,但取决于您的情况,这可能是可以接受的。

如果我们假设包含守卫总是看起来像

#define NAME /* no more tokens here */

如果如您所说,任何编译时错误(而不是 #error 排他性)是可以接受的,那么您可以执行以下操作:

#define THENAMESPACE BLA
#define BLA_API_H // Comment out to get a error.

#define CAT(x,y) CAT_(x,y)
#define CAT_(x,y) x##y

#define NAMESPACE(x) static int CAT(UNUSED_,__LINE__) = CAT(CAT(THENAMESPACE,CAT(_,x)),+1);

NAMESPACE(API_H)

此处,NAMESPACE(API_H) 尝试使用 ##.

连接 BLA_API_H+

这导致 error: pasting "BLA_API_H" and "+" does not give a valid preprocessing token 除了 如果 BLA_API_H#defined 到 'no tokens'。

#define BLA_API_H 的情况下,NAMESPACE(API_H) 就变成了

static int UNUSED_/*line number here*/ = +1;

如果您接受了一个不太健壮的解决方案,您甚至可以得到很好的错误消息:

#define THENAMESPACE BLA
#define BLA_API_H // Comment out to get a error.

#define TRUTHY_VALUE_X 1
#define CAT(x,y) CAT_(x,y)
#define CAT_(x,y) x##y

#define NAMESPACE(x) CAT(CAT(TRUTHY_VALUE_,CAT(THENAMESPACE,CAT(_,x))),X)

#if !NAMESPACE(API_H)
#error "namespace not properly defined"
#endif

此处,如果定义了BLA_API_H,则#if !NAMESPACE(API_H)扩展为#if 1

如果 BLA_API_H 未定义,则扩展为 #if TRUTHY_VALUE_BLA_API_HX,并且 TRUTHY_VALUE_BLA_API_HX 由于未定义而计算为 false

这里的问题是,如果 TRUTHY_VALUE_BLA_API_HX 不小心被定义为真实的东西,你就会得到一个假否定。

您也可以使用预处理器模式匹配来执行此操作。

#define PASTE3(A,B,C) PASTE3_I(A,B,C)
#define PASTE3_I(A,B,C) A##B##C
#define PASTE(A,B) PASTE_I(A,B)
#define PASTE_I(A,B) A##B
#define THIRD(...) THIRD_I(__VA_ARGS__,,,)
#define THIRD_I(A,B,C,...) C
#define EMPTINESS_DETECTOR ,
#if THIRD(PASTE3(EMPTINESS_,PASTE(THENAMESPACE,_API_H),DETECTOR),0,1)
#  error "namespace not properly defined"
#endif

这与@HolyBlackCat 的答案相同,只是它是在预处理器中完成的; #if 指令中表达式的内部粘贴根据 THENAMESPACE 生成一个标记,将其粘贴到您需要的 _API_H。如果该标记本身是在宏中定义的,它将在 PASTE3 操作期间扩展为地标;这有效地粘贴了 EMPTINESS_ [placemarker] DETECTOR,这是一个扩展为逗号的宏。该逗号将移动间接 THIRD 的参数,将 0 放在那里,使条件等同于 #if 0。其他任何内容都不会改变参数,这会导致 THIRD 选择 1,从而触发 #error.

这也做出了与 HolyBlackCat 的回答相同的假设...包含守卫总是看起来像 #define BLA_API_H,但您可以使用扩展模式匹配来适应特定的替代样式...例如,如果您想要接受像 #define BLAH_API_H 1 这样的包含保护(谁做的?),你可以添加 #define EMPTINESS_1DETECTOR ,.

除了使用 defined 运算符或等效项来测试标识符是否被定义为宏名称之外,该语言没有定义其他方法。特别是

形式的预处理器指令
#ifndef identifier

等同于

#if ! defined identifier

(C11, 6.10.1/5)。类似适用于 #ifdef.

defined 运算符将单个标识符作为其操作数,而不是表达式 (C11, 6.10.1/1). Moreover, although the expression associated with an #if directive is macro expanded prior to evaluation, the behavior is undefined if in that context macro expansion produces the token "defined", and macro names modified by the defined unary operator are explicitly excluded from expansion (C11, 6.10.1/4)。

因此,虽然在许多上下文中可以通过标记粘贴构造宏名称,并且在这种上下文中,结果随后会受到宏扩展的影响,但 defined 运算符的操作数不是这样的语境。因此,该语言没有定义测试构造标识符或 indirectly-specified 标识符是否定义为宏名称的方法。

但是,如果您控制了所有 header 守卫,则可以避免依赖 defined,并且您愿意稍微偏离传统风格。不要仅仅 #defineing 你的 header 守卫,而是将它们定义为 一些非零整数值,比如 1:

#if ! MYPREFIX_SOMEHEADER_H
#define MYPREFIX_SOMEHEADER_H 1

// body of someheader.h ...

#endif

然后您可以从测试表达式中删除 defined 运算符:

#if ! NAMESPACE(API_H)
#  error "namespace not properly defined"
#endif

但是请注意,#define 指令也有类似的问题:它定义了一个标识符,不受先前宏扩展的影响。因此,您不能动态构造 header 守卫名称。我不确定您是否考虑过这一点,但如果您考虑过,那么所有的预测可能都没有实际意义。