如何比较 C 条件预处理器指令中的字符串

我必须在 C 中做这样的事情。它只有在我使用字符时才能工作,但是我需要一个字符串。我怎么能这么做?

#define USER "jack" // jack or queen


#if USER == "jack"
#define USER_VS "queen"
#elif USER == "queen"
#define USER_VS "jack"
#endif
135751 次浏览

Use numeric values instead of strings.

最后,要将常量 JACK 或 QUEEN 转换为字符串,可以使用 stringize (和/或 tokenize)操作符。

我不认为有一种方法可以完全在预处理器指令中进行可变长度的字符串比较。你也许可以这样做:

#define USER_JACK 1
#define USER_QUEEN 2


#define USER USER_JACK


#if USER == USER_JACK
#define USER_VS USER_QUEEN
#elif USER == USER_QUEEN
#define USER_VS USER_JACK
#endif

或者你可以稍微重构一下代码,用 C 代码代替。

如果你的字符串是编译时间常量(在你的例子中) ,你可以使用以下技巧:

#define USER_JACK strcmp(USER, "jack")
#define USER_QUEEN strcmp(USER, "queen")
#if $USER_JACK == 0
#define USER_VS USER_QUEEN
#elif USER_QUEEN == 0
#define USER_VS USER_JACK
#endif

编译器可以预先告诉 strcmp 的结果,并用它的结果替换 strcmp,从而为您提供一个可以与预处理器指令进行比较的 # Definition。我不知道在编译器/依赖于编译器选项之间是否存在任何差异,但它在 GCC 4.7.2上对我很有用。

编辑: 经过进一步的研究,看起来这是一个工具链扩展,而不是 GCC 扩展,所以要考虑到这一点..。

[更新: 2021.01.04]

自从我在2014年第一次发表这篇文章以来,有一件事情发生了变化,那就是 #pragma message的格式。

现在,需要括号!

#pragma message ("USER    IS " USER)
#pragma message ("USER_VS IS " USER_VS)

也就是说,2016代码(使用字符,而不是字符串)仍然可以在 VS2019中使用。

但是,正如@Artyer 指出的,包含 c_strcmp的版本在任何现代编译器中都不能工作。

[UPDATE: 2018.05.03]

警告 : 并非所有编译器都以相同的方式实现 C + + 11规范。 下面的代码可以在我测试的编译器中工作,而许多评论者使用的是不同的编译器。

引用 Shafik Yaghmour 的回答: Computing length of a C string at compile time. Is this really a constexpr?

Constant expressions are not guaranteed to be evaluated at compile 时间,我们只有一个从草案 C + + 标准的非规范引用 第5.19节说明这一点的常量表达式:

[ ... ] > [注意: 常量表达式可以在 翻译

can这个词让世界变得完全不同。

因此,YMMV 在这个(或任何)答案中涉及到 constexpr,这取决于编译器编写者对规范的解释。

[更新于2016.01.31]

As some didn't like my earlier answer because it 避免 the whole compile time string compare aspect of the OP by accomplishing the goal with no need for string compares, here is a more detailed answer.

你不能!C98和 C99都没有。连 C11都没有。任何宏操作都不会改变这一点。

The definition of const-expression used in the #if does not allow strings.

它确实允许使用字符,所以如果你限制自己使用字符,你可以使用:

#define JACK 'J'
#define QUEEN 'Q'


#define CHOICE JACK     // or QUEEN, your choice


#if 'J' == CHOICE
#define USER "jack"
#define USER_VS "queen"
#elif 'Q' == CHOICE
#define USER "queen"
#define USER_VS "jack"
#else
#define USER "anonymous1"
#define USER_VS "anonymous2"
#endif


#pragma message "USER    IS " USER
#pragma message "USER_VS IS " USER_VS

你可以! 在 C + + 11中。如果你为比较定义了一个编译时助手函数。

[2021.01.04: CAVEAT: This does not work in any MODERN compiler. See comment by @Artyer.]

// compares two strings in compile time constant fashion
constexpr int c_strcmp( char const* lhs, char const* rhs )
{
return (('\0' == lhs[0]) && ('\0' == rhs[0])) ? 0
:  (lhs[0] != rhs[0]) ? (lhs[0] - rhs[0])
: c_strcmp( lhs+1, rhs+1 );
}
// some compilers may require ((int)lhs[0] - (int)rhs[0])


#define JACK "jack"
#define QUEEN "queen"


#define USER JACK       // or QUEEN, your choice


#if 0 == c_strcmp( USER, JACK )
#define USER_VS QUEEN
#elif 0 == c_strcmp( USER, QUEEN )
#define USER_VS JACK
#else
#define USER_VS "unknown"
#endif


#pragma message "USER    IS " USER
#pragma message "USER_VS IS " USER_VS

因此,最终,您必须改变为 USERUSER_VS选择最终字符串值的方式。

在 C99中不能进行编译时字符串比较,但是可以选择编译时字符串。

如果您真的必须进行编译时的刺比较,那么您需要更改为 C + + 11或更新的允许该特性的变体。

[原始答案跟随]

试试:

#define jack_VS queen
#define queen_VS jack


#define USER jack          // jack    or queen, your choice
#define USER_VS USER##_VS  // jack_VS or queen_VS


// stringify usage: S(USER) or S(USER_VS) when you need the string form.
#define S(U) S_(U)
#define S_(U) #U

UPDATE: ANSI 令牌粘贴有时并不明显

将单个 #置于宏之前会导致将其更改为其值的字符串,而不是其空值。

在两个令牌之间放置一个双 ##将导致它们连接到单个令牌中。

因此,根据您设置 USER的方式,宏 USER_VS具有扩展 jack_VSqueen_VS

串起来S(...)使用宏间接,因此命名宏的值被转换为字符串。而不是宏的名称。

因此,USER##_VS变成 jack_VS(或 queen_VS) ,这取决于您如何设置 USER

之后,当使用 串起来宏作为 S(USER_VS)时,USER_VS(本例中为 jack_VS)的值被传递给间接步骤 S_(jack_VS),后者将其值(queen)转换为字符串 "queen"

如果将 USER设置为 queen,那么最终结果是字符串 "jack"

有关标记连接,请参见: https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html

有关令牌字符串转换,请参见: https://gcc.gnu.org/onlinedocs/cpp/Stringification.html#Stringification

[更新2015.02.15以纠正打字错误。]

The answere by 帕特里克 and by 杰西 · 奇泽姆 made me do the following:

#define QUEEN 'Q'
#define JACK 'J'


#define CHECK_QUEEN(s) (s==QUEEN)
#define CHECK_JACK(s) (s==JACK)


#define USER 'Q'


[... later on in code ...]


#if CHECK_QUEEN(USER)
compile_queen_func();
#elif CHECK_JACK(USER)
compile_jack_func();
#elif
#error "unknown user"
#endif

代替 #define USER 'Q' #define USER QUEEN 也应该工作,但没有测试也可以工作,并且可能更容易处理。

EDIT: According to the comment of @Jean-François Fabre I adapted my answer.

如前所述,ISO-C11预处理器支持 没有字符串比较。然而,分配具有“相反值”的宏的问题可以通过“令牌粘贴”和“表访问”来解决。Jesse 的简单 concatenate/stringify 宏解决方案在 gcc 5.4.0中失败,因为字符串化完成了 之前对串联的评估(符合 ISO C11)。然而,这个问题是可以解决的:

#define P_(user) user ## _VS
#define VS(user) P_ (user)
#define S(U) S_(U)
#define S_(U) #U


#define jack_VS  queen
#define queen_VS jack


S (VS (jack))
S (jack)
S (VS (queen))
S (queen)


#define USER jack          // jack    or queen, your choice
#define USER_VS USER##_VS  // jack_VS or queen_VS
S (USER)
S (USER_VS)

第一行(宏 P_())添加了一个间接代码,让下一行(宏 VS())完成串联 之前的字符串化(参见 为什么宏需要双层间接?)。字符串化宏(S()S_())来自 Jesse。

与 OP 的 if-then-else 构造相比,表(宏 jack_VSqueen_VS)的维护要容易得多。

最后,下一个四行代码块调用函数样式的宏,最后一个四行代码块来自 Jesse 的回答。

将代码存储在 foo.c中并调用预处理器 gcc -nostdinc -E foo.c产生:

# 1 "foo.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "foo.c"
# 9 "foo.c"
"queen"
"jack"
"jack"
"queen"






"jack"
"USER_VS"

输出正如预期的那样。最后一行显示了在字符串化之前 USER_VS宏是 没有展开的。

很简单,我想你可以说

#define NAME JACK
#if NAME == queen

下面这些对我来说很管用。允许符号宏值比较。# error xxx只是为了看看编译器到底是做什么的。用 # 定义猫(a,b) a # # b代替 定义会破坏一些东西。

#define cat(a,...) cat_impl(a, __VA_ARGS__)
#define cat_impl(a,...) a ## __VA_ARGS__


#define xUSER_jack 0
#define xUSER_queen 1
#define USER_VAL cat(xUSER_,USER)


#define USER jack // jack or queen


#if USER_VAL==xUSER_jack
#error USER=jack
#define USER_VS "queen"
#elif USER_VAL==xUSER_queen
#error USER=queen
#define USER_VS "jack"
#endif
#define USER_IS(c0,c1,c2,c3,c4,c5,c6,c7,c8,c9)\
ch0==c0 && ch1==c1 && ch2==c2 && ch3==c3 && ch4==c4 && ch5==c5 && ch6==c6 && ch7==c7 ;


#define ch0 'j'
#define ch1 'a'
#define ch2 'c'
#define ch3 'k'


#if USER_IS('j','a','c','k',0,0,0,0)
#define USER_VS "queen"
#elif USER_IS('q','u','e','e','n',0,0,0)
#define USER_VS "jack"
#endif

它基本上是一个固定长度的静态字符数组手动初始化 取代了自动初始化的可变长度静态字符数组,该数组始终以结束空字符结束

如果 USER 被定义为带引号的字符串,则不能这样做。

但是如果用户只是 JACK 或者 QUEEN 或者 Joker 或者其他什么的,你 可以就会这样做。

有两个技巧可以使用:

  1. 令牌拼接,通过连接其字符将一个标识符与另一个标识符组合在一起。这允许您与 JACK 进行比较,而不必与某些东西进行 #define JACK比较
  2. variadic macro expansion, which allows you to handle macros with variable numbers of arguments. This allows you to expand specific identifiers into varying numbers of commas, which will become your string comparison.

So let's start out with:

#define JACK_QUEEN_OTHER(u) EXPANSION1(ReSeRvEd_, u, 1, 2, 3)

现在,如果我写 JACK_QUEEN_OTHER(USER),USER 是 JACK,预处理器 变成 EXPANSION1(ReSeRvEd_, JACK, 1, 2, 3)

第二步是连接:

#define EXPANSION1(a, b, c, d, e) EXPANSION2(a##b, c, d, e)

现在 JACK_QUEEN_OTHER(USER)变成了 EXPANSION2(ReSeRvEd_JACK, 1, 2, 3)

这样就有机会根据字符串是否匹配来添加一些逗号:

#define ReSeRvEd_JACK x,x,x
#define ReSeRvEd_QUEEN x,x

如果用户是 JACK,则 JACK_QUEEN_OTHER(USER)变成 EXPANSION2(x,x,x, 1, 2, 3)

如果用户是后,则 JACK_QUEEN_OTHER(USER)变为 EXPANSION2(x,x, 1, 2, 3)

如果用户是其他用户,则 JACK_QUEEN_OTHER(USER)变成 EXPANSION2(ReSeRvEd_other, 1, 2, 3)

此时,发生了一些重要的事情: EXPANSION2宏的第四个参数是1、2或3,这取决于最初传递的参数是 jack、 queen 还是其他任何参数。所以我们要做的就是把它挑出来。由于冗长的原因,最后一步我们需要两个宏; 它们是 EXPANSION2和 EXPANSION3,尽管其中一个似乎没有必要。

把它们放在一起,我们有这6个宏:

#define JACK_QUEEN_OTHER(u) EXPANSION1(ReSeRvEd_, u, 1, 2, 3)
#define EXPANSION1(a, b, c, d, e) EXPANSION2(a##b, c, d, e)
#define EXPANSION2(a, b, c, d, ...) EXPANSION3(a, b, c, d)
#define EXPANSION3(a, b, c, d, ...) d
#define ReSeRvEd_JACK x,x,x
#define ReSeRvEd_QUEEN x,x

你可以这样使用它们:

int main() {
#if JACK_QUEEN_OTHER(USER) == 1
printf("Hello, Jack!\n");
#endif
#if JACK_QUEEN_OTHER(USER) == 2
printf("Hello, Queen!\n");
#endif
#if JACK_QUEEN_OTHER(USER) == 3
printf("Hello, who are you?\n");
#endif
}

强制性 Godbolt 链接: https://godbolt.org/z/8WGa19


MSVC 更新: 你必须稍微不同的括号,使东西也在 MSVC 中工作。展开 * 宏看起来像这样:

#define EXPANSION1(a, b, c, d, e) EXPANSION2((a##b, c, d, e))
#define EXPANSION2(x) EXPANSION3 x
#define EXPANSION3(a, b, c, d, ...) d

强制性: https://godbolt.org/z/96Y8a1

While the pre-processor is very limited with respect to strings, most compilers know a lot about strings 在编译时也是如此. For instance this can successfully compare __BASE_FILE__ and __FILE__ at compile-time:

const int zero_div_warning __attribute__((unused)) =
42 / !strcmp(__FILE__ , __BASE_FILE__);

Compilation of this with gcc -Wno-div-by-zero -Werr succeeds when found in a .c file and fails when found in a .h file (static inline function)

- Wno-div-by-zero 是-Wall 的一部分

虽然这可能不能解决您的特定用例,但它确实打开了比较常量字符串 在编译时的许多可能性。

我知道从技术上来说这并不能回答 OP 的问题,但是通过看上面的答案,我意识到(从我所能理解的)在预处理器中进行字符串比较并不是一种简单的方法,除非借助一些“技巧”或者其他编译器特有的魔法。因此,在重新考虑我的情况时,我意识到实际上只有一组固定的字符串可以与之进行比较,因为预处理器无论如何都必须使用静态字符串。因此,它更多的是一个风格的东西,能够比较在您的代码中的“字符串”之类的东西。因此,我决定添加一些定义,它们的语法类似于字符串(在读取它时) ,但只是整数的定义,看起来就像其他人建议的那样。例如:

#if USER == USER_JACK
// do something
#elif USER == USER_QUEEN
// do something else
#elif USER == USER_KING
// do something completely different
#else
// abort abort
#end

So now it is just a question of setting up the definitions appropriately.

作为一个更具体的示例,我最初希望进行字符串比较,以便在使用 Cereal 序列化库时指定默认的归档类型。在 Cereal 中有3种有效的归档类型: JSON、 XML 和 Binary,我希望用户能够在 CMake 中将它们作为字符串变量输入。我仍然使这成为可能(并且使用 CMake 的 CACHE STRING 属性约束变量) ,但是在将字符串作为编译器定义传递之前,将它转换为一个整数。(我提前道歉,因为我知道这是以 CMake 为中心的,这不是最初问题的一部分。)

在 CMakeLists.txt 文件中,使用 CMake 自动化事情,我包含了以下 SetupCereal.CMake 脚本:

set( CEREAL_DIR "" CACHE PATH "Path to Cereal installation" )
set( CEREAL_INCLUDE_DIR ${CEREAL_DIR}/include )


# Set up the  user input variable and constrain to valid values
set( CEREAL_ARCHIVE_DEFAULT_TYPE "JSON"
CACHE STRING
"Default Archive type to use for Cereal serialization"
)
set_property( CACHE CEREAL_ARCHIVE_DEFAULT_TYPE
PROPERTY STRINGS JSON XML BINARY
)


# Convert the string to integer for preprocessor comparison
if ( "${CEREAL_ARCHIVE_DEFAULT_TYPE}" STREQUAL "JSON")
set( CEREAL_ARCHIVE_DEFAULT_TYPE_VALUE 0 )
elseif( "${CEREAL_ARCHIVE_DEFAULT_TYPE}" STREQUAL "XML" )
set( CEREAL_ARCHIVE_DEFAULT_TYPE_VALUE 1 )
elseif( "${CEREAL_ARCHIVE_DEFAULT_TYPE}" STREQUAL "BINARY" )
set( CEREAL_ARCHIVE_DEFAULT_TYPE_VALUE 2 )
endif()


# Setup the corresponding preprocessor definitions
set( CEREAL_DEFINES
-DCEREAL_ARCHIVE_JSON=0
-DCEREAL_ARCHIVE_XML=1
-DCEREAL_ARCHIVE_BINARY=2
-DCEREAL_ARCHIVE_DEFAULT_TYPE=${CEREAL_ARCHIVE_DEFAULT_TYPE_VALUE}
)

然后我做了一个附带的 CerealArchive.hpp 头文件,看起来像这样:

#pragma once


#if CEREAL_ARCHIVE_DEFAULT_TYPE == CEREAL_ARCHIVE_JSON
#  include <cereal/archives/json.hpp>


namespace cereal
{
using DefaultOutputArchive = JSONOutputArchive;
using DefaultInputArchive  = JSONInputArchive;
}


#elif CEREAL_ARCHIVE_DEFAULT_TYPE == CEREAL_ARCHIVE_XML
#  include <cereal/archives/xml.hpp>


namespace cereal {
using DefaultOutputArchive = XMLOutputArchive;
using DefaultInputArchive  = XMLInputArchive;
} // namespace cereal


#elif CEREAL_ARCHIVE_DEFAULT_TYPE == CEREAL_ARCHIVE_BINARY
#  include <cereal/archives/binary.hpp>


namespace cereal
{
using DefaultOutputArchive = BinaryOutputArchive;
using DefaultInputArchive  = BinaryInputArchive;
}


#endif // CEREAL_ARCHIVE_DEFAULT_TYPE

然后客户端代码是这样的:

#include <CerealArchive.hpp>
#include <sstream>


std::ostringstream oss;
{
cereal::DefaultOutputArchive archive( oss );
archive( 123 );
}
std::string s = oss.str();


然后,开发人员可以选择默认的 Archive 类型作为 CMake 字符串变量(当然后面是重新编译)。

So while technically this solution isn't comparing strings, syntactically it kind of behaves/looks the same.

I'm also thinking that the the SetupCereal.cmake could be further generalized to encapsulate the setup in a function, so it could be used in other situations where you want to define similar types of definitions.

#define USER "jack" // jack or queen
        

#ifdef USER \
if (USER == "jack")#define USER_VS "queen" \
else if(USER == "queen") #define USER_VS "jack"
#endif

我希望在预处理器宏中也使用字符串比较,主要是因为我还可以在预处理步骤中“打印”这些值(使用杂注消息)。

正如 接受的答案中提到的:

我不认为有一种方法可以完全在预处理器指令中进行可变长度的字符串比较。

... 但是,如果字符串值是以字符数组的形式写入的,那么它似乎是可行的,这既适用于“变量”/测试宏,也适用于表示要测试的值的宏。例如,我在 https://replit.com/languages/c中测试了以下代码:

#include <stdio.h>


#define XSTR(x) STR(x)
#define STR(x) #x


#define TEST_STR ('t','e','s','t')
#define OTHER_STR ('o','t','h','e','r')
#define MYMACRO ('t','e','s','t')


#pragma message( "MYMACRO is: " XSTR(MYMACRO) )
#if MYMACRO==TEST_STR
#pragma message( "IS TRUE" )
#else
#pragma message( "IS FALSE" )
#endif


int main() {
printf("Hello, world!\r\n");
return 0;
}

... and it outputs:

> clang-7 -pthread -lm -o main main.c
main.c:10:9: warning: MYMACRO is: ('t','e','s','t')
[-W#pragma-messages]
#pragma message( "MYMACRO is: " XSTR(MYMACRO) )
^
main.c:12:9: warning: IS TRUE [-W#pragma-messages]
#pragma message( "IS TRUE" )
^
2 warnings generated.
> ./main
Hello, world!
>

如果我把条件改成:

#if MYMACRO==OTHER_STR

然后重新编译,输出结果是:

> clang-7 -pthread -lm -o main main.c
main.c:10:9: warning: MYMACRO is: ('t','e','s','t')
[-W#pragma-messages]
#pragma message( "MYMACRO is: " XSTR(MYMACRO) )
^
main.c:14:9: warning: IS FALSE [-W#pragma-messages]
#pragma message( "IS FALSE" )
^
2 warnings generated.
> ./main
Hello, world!
>

So, if the tested macro and the value macros are defined as lists of character values, then they can be compared directly in a preprocessor #if conditional - and in order to print such macros in a pragma message, use of stringifying macros are required.

注意,当在杂注消息中打印这样的宏时,逗号和单引号仍然是可见的(我想可以使用可变的宏/__VA_ARGS__来创建一个宏,它将循环通过字符数组值并以更易读的方式连接它们,但是上面的技术对我来说已经足够好了)。