测试宏内预处理符号是否已定义

8
通常测试预处理器符号是否已定义的方法是使用 #ifdef。然而,在宏中无法使用 #ifdef。我需要的是一种方法,在宏中检查该宏的参数是否为已定义的预处理器符号。
例如:
#define TRACE(x,y) if(IS_DEFINED(x)){ std::cout << y; }

在这里,TRACE有两个参数,第一个x应该是预处理器符号的名称。如果定义了这样的符号,则应打印第二个参数。我要找的是不存在的IS_DEFINED函数/宏。
用法如下:
#undef BLA
TRACE(BLA,"abc") // "abc" won't be printed, as BLA is not defined
#define BLA 1
TRACE(BLA,"xyz") // "xyz" will be printed, as BLA is a defined symbol

有没有办法实现这个?也许有一些宏的魔法可以做到?当然,这个解决方案应该适用于任何符号,而不仅仅是BLA或一组硬编码的符号。如果要检查的符号集事先已知,那么显然很容易实现。

1
你可以使用一个变量,并依赖编译器来优化掉条件语句吗? - BonzaiThePenguin
@BonzaiThePenguin:不确定你的意思是什么。请举个例子。 - gexicide
6个回答

8

将宏的字符串形式(名称)与宏的字符串形式(展开后的值)进行比较:

#include <iostream>
#include <cstring>

#define TRACE_STRINGIFY(item) "" #item
#define TRACE(macro, message)                          \
    do {                                               \
        if (strcmp("" #macro, TRACE_STRINGIFY(macro))) \
            std::cout << message << "\n";              \
    } while (0)
"" # 宏 将宏名展开为字符串,而 TRACE_STRINGIFY(macro) 首先展开宏,然后将结果转化为字符串。如果两者不同,则 macro 必须是一个预处理器宏。
这种方法对于定义为自身的宏(例如#define FOO FOO)会失败。这样的宏不会被检测为预处理器宏。
大多数编译器应该能够完全优化掉两个字符串字面值的比较。GNU GCC (g++) 4.8.2 在 -O0 下肯定可以(gcc for C 也一样 -- 这种方法在 C 语言中同样有效)。
这种方法对于函数式宏确实有效,但只有在保留括号(如果宏有多个参数,则还需要正确的逗号数)且未定义为自身时才有效(例如 #define BAR(x) BAR(x))。
例如:
#define TEST1 TEST1

#define TEST3
#define TEST4 0
#define TEST5 1
#define TEST6 "string"
#define TEST7 ""
#define TEST8 NULL
#define TEST9 TEST3
#define TEST10 TEST2
#define TEST11(x)

#define TEST13(x,y,z) (x, y, z)


int main(void)
{
    TRACE(TEST1, "TEST1 is defined");
    TRACE(TEST2, "TEST2 is defined");
    TRACE(TEST3, "TEST3 is defined");
    TRACE(TEST4, "TEST4 is defined");
    TRACE(TEST5, "TEST5 is defined");
    TRACE(TEST6, "TEST6 is defined");
    TRACE(TEST7, "TEST7 is defined");
    TRACE(TEST8, "TEST8 is defined");
    TRACE(TEST9, "TEST9 is defined");
    TRACE(TEST10, "TEST10 is defined");
    TRACE(TEST11, "TEST11 is defined");
    TRACE(TEST12, "TEST12 is defined");
    TRACE(TEST13, "TEST13 is defined");
    TRACE(TEST14, "TEST14 is defined");

    TRACE(TEST1(), "TEST1() is defined");
    TRACE(TEST2(), "TEST2() is defined");
    TRACE(TEST3(), "TEST3() is defined");
    TRACE(TEST4(), "TEST4() is defined");
    TRACE(TEST5(), "TEST5() is defined");
    TRACE(TEST6(), "TEST6() is defined");
    TRACE(TEST7(), "TEST7() is defined");
    TRACE(TEST8(), "TEST8() is defined");
    TRACE(TEST9(), "TEST9() is defined");
    TRACE(TEST10(), "TEST10() is defined");
    TRACE(TEST11(), "TEST11() is defined");
    TRACE(TEST12(), "TEST12() is defined");
    TRACE(TEST13(,,), "TEST13(,,) is defined");
    TRACE(TEST14(,,), "TEST14(,,) is defined");

    return 0;
}

它输出

TEST3 is defined
TEST4 is defined
TEST5 is defined
TEST6 is defined
TEST7 is defined
TEST8 is defined
TEST9 is defined
TEST10 is defined
TEST3() is defined
TEST4() is defined
TEST5() is defined
TEST6() is defined
TEST7() is defined
TEST8() is defined
TEST9() is defined
TEST10() is defined
TEST11() is defined
TEST13(,,) is defined

换句话说,未将TEST1符号识别为已定义(因为它被定义为自身),不带括号的TEST11TEST13也是如此。这些是这种方法的限制。
对于所有无参数宏(除了TEST1,即那些被定义为自身的宏)和所有单参数宏,使用括号形式都有效。如果一个宏期望多个参数,您需要使用正确数量的逗号,否则(例如,如果您尝试TRACE(TEST13(),"...")),您将得到一个编译时错误:“宏TEST13需要3个参数,但只给出了1个”或类似的信息。
还有问题吗?

有趣,但是你怎么知道字符串字面值的比较在编译时被优化掉了呢?(最好有一些硬性参考,汇编检查只是个案例) - hmijail
好的,我能找到的最好的参考资料是GCC内置函数手册页面上并没有什么具体的说明。但是,GCC 5.2 -O0和Clang 3.8 -O1都会在编译时评估strcmp。这对于嵌入式开发非常有用!我写了一篇关于如何以更通用的方式在普通C中使用它的博客文章:http://hmijailblog.blogspot.com/2016/03/an-isdefined-c-macro-to-check-whether.html - hmijail
@hmijail:我喜欢你博客文章中的__builtin_strcmp()方法;它最有可能在编译时被评估。总的来说,cpp(C预处理器)缺乏字符串比较功能很烦人,也有点令人惊讶。然而,如果我们将自己限制在数字触发/规则宏上,一切都会变得简单得多。我特别喜欢GNU C库使用#define _POSIX_C_SOURCE YYYYMML来控制导出哪个版本的POSIX C特性的方式。(L是为了使其始终为long。) - Nominal Animal

7

Linux中的kgconfig.h为此情况定义了一个__is_defined宏:

 #define __ARG_PLACEHOLDER_1 0,
 #define __take_second_arg(__ignored, val, ...) val

/*
 * Helper macros to use CONFIG_ options in C/CPP expressions. Note that
 * these only work with boolean and tristate options.
 */

/*
 * Getting something that works in C and CPP for an arg that may or may
 * not be defined is tricky.  Here, if we have "#define CONFIG_BOOGER 1"
 * we match on the placeholder define, insert the "0," for arg1 and generate
 * the triplet (0, 1, 0).  Then the last step cherry picks the 2nd arg (a one).
 * When CONFIG_BOOGER is not defined, we generate a (... 1, 0) pair, and when
 * the last step cherry picks the 2nd arg, we get a zero.
 */
#define __is_defined(x)         ___is_defined(x)
#define ___is_defined(val)      ____is_defined(__ARG_PLACEHOLDER_##val)
#define ____is_defined(arg1_or_junk)    __take_second_arg(arg1_or_junk 1, 0)

这是C99版本,适用于三态选项(未定义、定义为0、定义为1)。


这仅适用于布尔宏。对于定义字符串、整数或引用其他宏的宏,它将失败。#define EXAMPLE_MACRO_NO_VALUE #define EXAMPLE_MACRO_VALUE_IS_NAME EXAMPLE_MACRO_VALUE_IS_NAME - The Matt

5

示例代码:

#include <iostream>

#define TRACE(name, msg) TRACE_EVAL_(TRACE_DO_, name, msg)
#define TRACE_EVAL_(macro, ...) macro(__VA_ARGS__)
#define TRACE_DO_(name, msg) \
    (#name[0] == 0 || #name[0] == '1' ? (void)(std::cout << (msg)) : (void)0)

#undef  FOO
#define BAR
#define BAZ 1

int main() {
    TRACE(FOO, "foo\n");
    TRACE(BAR, "bar\n");
    TRACE(BAZ, "baz\n");
}

根据宏定义的可能值,适当调整TRACE_DO_中的测试。请注意,支持非数字值的定义可能会有问题,因为很难将它们与宏名称区分开来...


1
仅适用于 #define FOO <数字>。例如,不适用于 #define FOO yes。Nominal Animal理解正确。 - hmijail

3
如果您可以将BLA始终定义为01(或其他可转换为bool的值),就可以进行以下操作。
#define TRACE(x, y) if (x) { std::cout << y << std::endl; }

一款优秀的编译器会优化 if 中的常量表达式,因此这种方法不会增加额外开销。 更新: 可能未定义宏的代码:
#define IS_DEFINED(x) IS_DEFINED2(x)
#define IS_DEFINED2(x) (#x[0] == 0 || (#x[0] >= '1' && #x[0] <= '9'))
#define TRACE(x, y) if (IS_DEFINED(x)) { std::cout << y << std::endl; }

演示

请注意,它仅在未定义FOO或将其定义为空或定义为某个数字时才起作用。

工作原理
对于函数式宏,展开方式如下:首先展开所有宏参数,除非它们与###一起使用,然后展开宏本身(请参见C++标准的第16.3.1章或C标准的6.10.3.1章中的正式解释)。

因此,IS_DEFINED(x)会使用已展开的宏x“调用”IS_DEFINED2(x)。如果x被定义,它将被替换为其定义内容,否则将按原样传递。

如果直接调用IS_DEFINED2(FOO),那么#x将始终等于"FOO",这不是您想要的。


是的,这很简单。这也是我目前正在做的事情。然而,这相当不方便。有一个适用于未定义符号的解决方案会让事情变得更容易。 - gexicide
哇,看起来不错。但是它是如何工作的?为什么我们需要另一个 IS_DEFINED2?我知道它看起来像 stringify,但我仍然无法完全理解它 :D。 - gexicide
@gexicide 我在答案中添加了解释。 - Anton Savin
为什么要像 #x[0] 这样构造? - zoska
1
@zoska 它测试 x 的字符串表示中的第一个字符。 - Anton Savin
仅适用于 #define FOO <数字>。例如,不适用于 #define FOO yes - hmijail

2
基于Christoph的答案,我制作了另一种版本,允许将可变参数传递到日志记录方法中(在这种情况下是Objective-C,但也应该适用于C++)。
#define TGLog(tag, msg, ...) TGLog_eval_(TGLog_do_, tag, msg, ## __VA_ARGS__)
#define TGLog_eval_(macro, ...) macro(__VA_ARGS__)
#define TGLog_do_(tag, msg, ...) \
(#tag[0] == 0 || #tag[0] == '1') ? NSLog(@"%s",[NSString stringWithFormat:msg, ## __VA_ARGS__]) : (void)0;

在可变参数变量前添加“##”可以在没有任何参数的情况下删除前面的逗号。这可以确保由于尾随逗号而导致扩展失败。

1
这个页面上的大多数解决方案都不是纯预处理器解决方案 - 至少部分测试发生在C(或C++或Objective-C)代码中,而不是在预处理器中。虽然这可能对某些用例是可接受的,但对于其他用例来说,这是一个很大的限制 - 特别是如果您试图编写一个只在定义了某些宏时生成代码(声明/语句等)的宏。Linux内核的__is_defined宏是一个纯预处理器解决方案,但它只适用于布尔宏(-DENABLE_FOO=1),它不能测试是否定义了宏(-DENABLE_FOO)。
使用Paul Fultz的cloak.h,我们可以创建一个纯预处理器解决方案,但适用于各种场景:
#include "cloak.h"
#define WHEN_DEFINED(token,then) \
   WHEN(NOT_EQUAL(DEFINED__##token,CAT(DEFINED__,token)))(then)

基本上,我们正在做的是将DEFINED__与令牌粘贴两次,一次进行宏展开,另一次不展开,然后比较它们。如果宏展开为其他内容,则它们将不同,在这种情况下,我们会展开then子句。
为了使此方法正常工作,我们需要为要测试的每个宏定义一个帮助器宏;例如,要测试TEST1,我们需要:
#define COMPARE_DEFINED__TEST1(x) x

现在我们可以做:
WHEN_DEFINED(TEST1,only_called_if_TEST1_defined());

这种方法只有在TEST1被定义为以下任何一种情况时才有效:
  • 空(#define TEST1
  • 任何非负整数(#define TEST1 0
  • 一个不同于它本身的标识符(#define TEST1 TEST2
如果TEST1被定义为以下情况,则此方法将无效:
  • 它本身(#define TEST1 TEST1)- 在这种情况下,它将认为TEST1未定义
  • 字符串、负整数、浮点数文字、数组、结构、运算符/标点符号、复杂表达式等 - 在这些情况下,它将失败并报告预处理器错误
  • 类似函数的宏(无论其扩展是什么),例如#define TEST1() 1
就我所知,前两个限制是无法避免的,因为标准C预处理器提供的功能有限。(我不确定是否支持类似函数的宏,但我怀疑很少有人需要这种功能。)
一个潜在的陷阱:如果我们忘记为某个标识符X正确定义COMPARE_DEFINED__帮助宏,它将默默失败(将X视为未定义,即使它实际上已经定义)。我们可以修改它,如果缺少帮助宏则发出预处理器错误:
#define ERROR(err) FATAL_ERROR_##err##!
#define ASSERT(cond,err) IF(cond)(EAT,ERROR)(err)
#define ASSERT_COMPARABLE(token) \
   ASSERT(IS_COMPARABLE(token),NOT_DEFINED__COMPARE_##token)
#define WHEN_DEFINED(token,then) \
   ASSERT_COMPARABLE(DEFINED__##token) \
   WHEN(NOT_EQUAL(DEFINED__##token,CAT(DEFINED__,token)))(then) 

现在,如果我们尝试在没有正确定义COMPARE_DEFINED__TEST3(x)的情况下执行WHEN_DEFINED(TEST3,something()),我们将会得到一个预处理器错误,如下所示:
error: pasting formed 'FATAL_ERROR_NOT_DEFINED__COMPARE_DEFINED__TEST3!', an invalid preprocessing token

(注:我已经使用clang和GCC进行了测试,两者都可以正常工作。我不知道它是否适用于其他编译器,如MSVC或Intel,可能不行。)

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