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

Posted

技术标签:

【中文标题】如何比较 C 条件预处理器指令中的字符串【英文标题】:How to compare strings in C conditional preprocessor-directives 【发布时间】:2011-01-21 02:37:17 【问题描述】:

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

#define USER "jack" // jack or queen

#if USER == "jack"
#define USER_VS "queen"
#elif USER == "queen"
#define USER_VS "jack"
#endif

【问题讨论】:

为什么不能只使用strcmp? @Brian:是的,我也读过这个问题 :-)。只是想确保他知道 strcmp 存在,并且响应可能很有启发性,因为我想不出这样做#define 的理由。 只是想提一下,同样的事情也适用于常规代码,而不仅仅是预处理器。当一个简单的值就行时,永远不要使用字符串。字符串比整数或枚举有更多的开销,如果你不需要做任何比比较它们更多的事情,那么字符串是错误的解决方案。 如果问题包含更多关于期望行为与实际行为的信息,那将很方便。 【参考方案1】:

使用数值而不是字符串。

最后要将常量 JACK 或 QUEEN 转换为字符串,请使用字符串化(和/或标记化)运算符。

【讨论】:

【参考方案2】:

我认为没有办法完全在预处理器指令中进行可变长度字符串比较。不过,您也许可以执行以下操作:

#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_VS (3 - USER) 在这种特定情况下。 :)【参考方案3】:

如果您的字符串是编译时常量(如您的情况),您可以使用以下技巧:

#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 替换为其结果,从而为您提供可以与预处理器指令进行比较的#define。我不知道编译器/对编译器选项的依赖之间是否存在差异,但它在 GCC 4.7.2 上对我有用。

编辑:经过进一步调查,看起来这是一个工具链扩展,而不是 GCC 扩展,因此请考虑到这一点......

【讨论】:

这当然不是标准的 C,而且我看不出它如何与任何编译器一起工作。编译器有时可以告诉表达式的结果(甚至函数调用,如果它们是内联的),但不能告诉预处理器。您是否使用了$ 某种预处理器扩展? 看起来 '#if $USER_JACK == 0' 语法有效,至少对于用于构建本机 android 代码 (JNI) 的 GNU C++...我不知道,但它非常有用,谢谢你告诉我们! 我在 GCC 4.9.1 上试过这个,我不相信这会像你想象的那样。虽然代码会编译,但它不会给你预期的结果。 '$' 被视为变量名。所以预处理器正在寻找'$USER_JACK'变量,没有找到它并给它默认值0。因此,无论strcmp如何,你总是将USER_VS定义为USER_QUEEN【参考方案4】:

[更新:2021.01.04]

自从我在 2014 年首次发布此内容以来,有一点发生了变化,那就是 #pragma message 的格式。

现在,括号是必需的!

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

也就是说,2016 年的代码(使用字符,而不是字符串)在 VS2019 中仍然有效。

但是,正如@Artyer 指出的那样,涉及c_strcmp 的版本将不适用于任何现代编译器。

[更新:2018.05.03]

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

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

不保证在编译时计算常量表达式 时间,我们只有 C++ 标准草案的非规范性引用 但是第 5.19 节常量表达式说明了这一点:

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

can 这个词让世界变得与众不同。

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

[2016.01.31 更新]

由于有些人不喜欢我之前的回答,因为它通过实现目标而无需字符串比较避免整个compile time string compare OP 方面,这里有一个更详细的答案。

你不能!不在 C98 或 C99 中。甚至在 C11 中也没有。再多的宏操作也无法改变这一点。

#if 中使用的const-expression 的定义不允许使用字符串。

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

#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

更新: ANSI 令牌粘贴有时不太明显。 ;-D

在宏之前放置一个 # 会导致它变成一个字符串,而不是它的裸值。

在两个标记之间放置一个双 ## 会使它们连接成一个标记。

因此,宏 USER_VS 具有扩展 jack_VSqueen_VS,具体取决于您如何设置 USER

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

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

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

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

有关令牌连接,请参阅:https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html

token字符串转换见:https://gcc.gnu.org/onlinedocs/cpp/Stringification.html#Stringification

[2015.02.15 更新以更正错字。]

【讨论】:

@JesseChisholm,你检查你的 C++11 版本了吗?我无法让它在 GCC 4.8.1、4.9.1、5.3.0 上运行。它在 #if 0 == c_strmp/*here*/( USER, QUEEN ) 上说 missing binary operator before token "(" @JesseChisholm 因此,如果我将 #if 0 == c_strcmp( USER, JACK ) 更改为 constexpr int comp1 = c_strcmp( USER, JACK ); #if 0 == comp1,我设法编译了您的 C++11 示例 @JesseChisholm,嗯,还是没有运气。 #if 中的任何 constexpr 变量都为零。您的示例仅因为 USER 是 JACK 而有效。如果 USER 是 QUEEN,它会说 USER IS QUEENUSER_VS IS QUEEN 这个答案的这个 c++11 部分是错误的。你不能从预处理指令调用函数(甚至是constexpr)。 这个完全错误的答案已经误导了引用它的人。您不能从预处理器调用 constexpr 函数; constexpr 直到翻译阶段 7 才被识别为关键字。预处理在翻译阶段 4 完成。【参考方案5】:

Patrick 和 Jesse Chisholm 的回答让我做了以下事情:

#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 应该也可以,但未经测试也可以,并且可能更容易处理。

编辑:根据@Jean-François Fabre 的评论,我调整了我的答案。

【讨论】:

change (s==QUEEN?1:0) by (s==QUEEN) 你不需要三元,结果已经是一个布尔值 是的,这种风格有效,因为#if 是比较字符,而不是字符串。该规范不允许使用字符串。【参考方案6】:

如上所述,ISO-C11 预处理器支持字符串比较。但是,使用“token paste”和“table access”可以解决分配“相反值”的宏的问题。 Jesse 的简单连接/字符串化宏解决方案在 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())在字符串化之前完成串联(参见Why do I need double layer of indirection for macros?)。字符串化宏(S()S_())来自 Jesse。

比 OP 的 if-then-else 构造更容易维护的表(宏 jack_VSqueen_VS)来自 Jesse。

最后,下一个四行代码块调用函数式宏。最后四行来自 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 宏在字符串化之前展开。

【讨论】:

这很好用,直到我尝试实际 比较 生成的字符串,以进行条件编译:#if (S(USER)=="jack") - 使用 " 时出现预处理器错误- error: invalid token at start of a preprocessor expression. 我必须将 #pragma message( ... ) 包裹在对 S(...) 的显示调用中,以便编译并获得您的结果。【参考方案7】:

这很简单,我想你可以直接说

#define NAME JACK    
#if NAME == queen 

【讨论】:

【参考方案8】:

以下内容对我有用clang。允许显示为符号宏值比较的内容。 #error xxx 只是为了看看编译器到底做了什么。用 #define cat(a,b) a ## b 替换 cat 定义会破坏事情。

#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

【讨论】:

不确定这是邪恶的、聪明的还是两者兼而有之,但这正是我想要的——谢谢!另一个有用的技巧是从 1 开始 #define 您的 xUSER_ 宏。然后您可以在 #elsif 列表的末尾添加一个 #else 子句,以捕获 USER 被意外设置为您不知道如何处理的情况。 (否则,如果你从 0 开始编号,那么 0 的情况就会成为你的全部,因为这是预处理器对未定义符号的默认数值。)【参考方案9】:
#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

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

【讨论】:

【参考方案10】:

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

但是如果 USER 只是 JACK 或 QUEEN 或 Joker 或其他任何人,您可以这样做。

有两个技巧可以使用:

    标记拼接,您只需将标识符与另一个标识符连接起来即可。这使您无需 #define JACK 就可以与 JACK 进行比较 可变参数宏扩展,它允许您处理具有可变数量参数的宏。这允许您将特定标识符扩展为不同数量的逗号,这将成为您的字符串比较。

让我们开始吧:

#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

如果 USER 是 JACK,JACK_QUEEN_OTHER(USER) 变为 EXPANSION2(x,x,x, 1, 2, 3)

如果 USER 是 QUEEN,JACK_QUEEN_OTHER(USER) 变为 EXPANSION2(x,x, 1, 2, 3)

如果 USER 是其他,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

必选神螺栓链接:https://godbolt.org/z/8WGa19


MSVC 更新:您必须稍微不同的括号才能使事情在 MSVC 中也能正常工作。 EXPANSION* 宏如下所示:

#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

【讨论】:

令人愉快的复杂!同样,它之所以有效,是因为您没有比较 #if 中的字符串。【参考方案11】:

虽然预处理器在字符串方面非常有限,但大多数编译器在编译时也对字符串了解很多。例如,这可以在编译时成功比较 __BASE_FILE____FILE__

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

使用gcc -Wno-div-by-zero -Werr 编译此代码在 .c 文件中成功,在 .h 文件中编译失败(静态内联函数)

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

虽然这可能无法解决您的特定用例,但它确实为比较常量字符串提供了许多可能性在编译时

【讨论】:

【参考方案12】:

我知道从技术上讲这不是回答 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

所以现在只是适当设置定义的问题。

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

使用 CMake 实现自动化,在 CMakeLists.txt 文件中,我包含以下 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();

然后,开发人员可以选择默认存档类型作为 CMake 字符串变量(当然,随后需要重新编译)。

因此,虽然从技术上讲,这个解决方案不是比较字符串,但从语法上讲,它的行为/看起来是一样的。

我还认为 SetupCereal.cmake 可以进一步推广以将设置封装在一个函数中,因此它可以用于您想要定义类似类型定义的其他情况。

【讨论】:

以上是关于如何比较 C 条件预处理器指令中的字符串的主要内容,如果未能解决你的问题,请参考以下文章

预处理指令typedef条件编译多文件代码

如何从预处理器#if 指令调用 constexpr 函数?

如何为 C# 生成自定义 StyleCop 规则 - 条件编译预处理器指令

C#-#define条件编译

C#-C#-#define条件编译

用于区分 Visual Studio 2012 和 2010 的 C# 预处理器指令或条件?