## 预处理器运算符的应用及需要考虑的注意事项是什么?

90

正如我在之前的许多问题中提到的那样,我正在学习《C程序设计语言》并且现在正在学习预处理器部分。其中一个更有趣的东西——这是我以前尝试学习C时从未知道过的——就是 ## 预处理器运算符。根据K&R的描述:

预处理器运算符 ## 提供了一种在宏扩展期间连接实际参数的方式。如果替换文本中的参数与 ## 相邻,则该参数将被实际参数替换,## 及其周围的空格将被删除,然后重新扫描结果。例如,宏 paste 连接它的两个参数:

#define paste(front, back) front ## back

因此,paste(name, 1) 创建令牌 name1

在实际世界中,人们会如何使用它,为什么会用到它?有哪些实际应用示例,需要注意哪些问题?

13个回答

51

在使用token-paste('##')或stringizing('#')预处理运算符时需要注意的一点是,为了在所有情况下正常工作,您必须使用额外的间接层级。

如果您不这样做,并且传递给token-pasting运算符的项目本身是宏,则可能得到您不想要的结果:

#include <stdio.h>

#define STRINGIFY2( x) #x
#define STRINGIFY(x) STRINGIFY2(x)
#define PASTE2( a, b) a##b
#define PASTE( a, b) PASTE2( a, b)

#define BAD_PASTE(x,y) x##y
#define BAD_STRINGIFY(x) #x

#define SOME_MACRO function_name

int main() 
{
    printf( "buggy results:\n");
    printf( "%s\n", STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( PASTE( SOME_MACRO, __LINE__)));

    printf( "\n" "desired result:\n");
    printf( "%s\n", STRINGIFY( PASTE( SOME_MACRO, __LINE__)));
}

输出:

buggy results:
SOME_MACRO__LINE__
BAD_PASTE( SOME_MACRO, __LINE__)
PASTE( SOME_MACRO, __LINE__)

desired result:
function_name21

1
有关此预处理器行为的说明,请参见 https://dev59.com/V2sy5IYBdhLWcg3w6yQN - Adam Davis
@MichaelBurr 我在阅读你的答案,我有一个疑问。为什么这个 LINE 打印出了行号? - HELP PLZ
3
@AbhimanyuAryan:我不确定你想问的是什么,但__LINE__是一个特殊的宏名称,它会被预处理器替换为源文件中当前行的行号。 - Michael Burr
如果能够像这里那样引用/链接语言规范会很酷。 - Antonio

47

CrashRpt:使用##将宏多字节字符串转换为Unicode

在CrashRpt(崩溃报告库)中有一个有趣的用法,如下所示:

#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
//Note you need a WIDEN2 so that __DATE__ will evaluate first.

他们希望使用双字节字符串代替每个字符一个字节的字符串。这可能看起来毫无意义,但他们有充分的理由这样做。
 std::wstring BuildDate = std::wstring(WIDEN(__DATE__)) + L" " + WIDEN(__TIME__);

他们将它与另一个返回日期和时间字符串的宏一起使用。
__ DATE __旁边放置L会导致编译错误。
Windows:使用##来表示通用Unicode或多字节字符串 Windows使用类似以下内容的语句:
#ifdef  _UNICODE
    #define _T(x)      L ## x
#else
    #define _T(x) x
#endif

_T在代码中被广泛使用。


各种库,用于定义清晰的访问器和修改器名称:

我也看到它被用来定义访问器和修改器的代码中:

#define MYLIB_ACCESSOR(name) (Get##name)
#define MYLIB_MODIFIER(name) (Set##name)

同样,您可以使用相同的方法来创建任何其他类型的聪明名称。
各种库,使用它来一次性进行多个变量声明:
#define CREATE_3_VARS(name) name##1, name##2, name##3
int CREATE_3_VARS(myInts);
myInts1 = 13;
myInts2 = 19;
myInts3 = 77;

3
由于您可以在编译时连接字符串文字,因此您可以将BuildDate表达式简化为 std::wstring BuildDate = WIDEN(__DATE__) L" " WIDEN(__TIME__); 并隐式一次性构建整个字符串。 - user666412

14

在升级编译器版本时,我遇到了一个需要注意的问题:

不必要地使用符号拼接运算符(##)是不可移植的,并且可能会产生不必要的空格、警告或错误。

当符号拼接运算符的结果不是有效的预处理器标记时,该运算符是不必要且可能有害的。

例如,有人可能试图使用符号拼接运算符在编译时构建字符串字面量:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a##+##b)
#define NS(a, b) STRINGIFY(a##::##b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

在一些编译器上,这将输出预期的结果:

1+2 std::vector
在其他编译器中,这将包括不必要的空格:
1 + 2 std :: vector

相当现代的GCC版本(>=3.3左右)将无法编译此代码:

foo.cpp:16:1: pasting "1" and "+" does not give a valid preprocessing token
foo.cpp:16:1: pasting "+" and "2" does not give a valid preprocessing token
foo.cpp:16:1: pasting "std" and "::" does not give a valid preprocessing token
foo.cpp:16:1: pasting "::" and "vector" does not give a valid preprocessing token
解决方法是在将预处理器标记连接到C/C++操作符时省略标记粘贴运算符:
#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a+b)
#define NS(a, b) STRINGIFY(a::b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

关于标记粘贴运算符,GCC CPP文档的章节提供了更多有用的信息。


谢谢 - 我不知道这个(但我不太使用这些预处理运算符...)。 - Michael Burr
3
这个运算符被称为“token pasting”操作符有其原因——目的是在完成后得到单个标记。写得不错。 - Mark Ransom
1
当 token-pasting 运算符的结果不是有效的预处理器标记时,其行为是未定义的。 - alecov
编程语言的变化如十六进制浮点数,或者(在C ++中)数字分隔符和用户定义字面量不断改变了什么构成“有效预处理标记”,因此请永远不要像那样滥用它!如果您必须分离(语言适当的)标记,请将它们拼写为两个单独的标记,并且不要依赖于预处理器语法和适当的语言之间的意外交互。 - Kerrek SB

7

在各种情况下,避免不必要的重复是很有用的。以下是来自Emacs源代码的例子。我们想从库中加载多个函数。函数“foo”应该分配给fn_foo,以此类推。我们定义了以下宏:

#define LOAD_IMGLIB_FN(lib,func) {                                      \
    fn_##func = (void *) GetProcAddress (lib, #func);                   \
    if (!fn_##func) return 0;                                           \
  }

我们可以随后使用它:
LOAD_IMGLIB_FN (library, XpmFreeAttributes);
LOAD_IMGLIB_FN (library, XpmCreateImageFromBuffer);
LOAD_IMGLIB_FN (library, XpmReadFileToImage);
LOAD_IMGLIB_FN (library, XImageFree);

好处是不必同时编写fn_XpmFreeAttributes"XpmFreeAttributes"(并且有错拼其中之一的风险)。


4

之前在Stack Overflow上有一个问题,询问如何平滑地生成枚举常量的字符串表示,而不需要大量容易出错的重复操作。

链接

我对那个问题的回答展示了如何应用一些预处理器技巧让你可以像这样定义枚举(例如)...;

ENUM_BEGIN( Color )
  ENUM(RED),
  ENUM(GREEN),
  ENUM(BLUE)
ENUM_END( Color )

使用宏扩展的好处不仅在于定义了枚举(在.h文件中),还定义了一个相应的字符串数组(在.c文件中);

const char *ColorStringTable[] =
{
  "RED",
  "GREEN",
  "BLUE"
};

字符串表的名称来自使用##运算符将宏参数(即Color)粘贴到StringTable中。像这样的应用程序(技巧?)是#和##运算符无价的地方。

3
您可以使用令牌粘贴来将宏参数与其他内容连接起来,这在需要进行模板操作时非常有用。
#define LINKED_LIST(A) struct list##_##A {\
A value; \
struct list##_##A *next; \
};

在这种情况下,LINKED_LIST(int) 将为您提供
struct list_int {
int value;
struct list_int *next;
};

同样,您可以编写一个用于列表遍历的函数模板。

2

SGlib 使用 ## 在 C 语言中基本模拟了模板。由于没有函数重载,## 被用于将类型名粘合到生成的函数名称中。如果我有一个叫做 list_t 的列表类型,那么我将得到类似 sglib_list_t_concat 等函数的名称。


2

我在嵌入式非标准C编译器上使用它来进行自定义断言:



#define ASSERT(exp) if(!(exp)){ \
                      print_to_rs232("断言失败: " ## #exp );\
                      while(1){} //让看门狗杀死我们 



3
你的意思是“非标准”指编译器没有执行字符串粘接,但执行了记号粘接——或者即使没有##也能正常工作?请确认我的理解是否正确。 - PJTraill

2
主要用途是当您有一个命名约定并且希望您的宏利用该命名约定时。也许您有几个方法族:image_create()、image_activate()和image_release(),还有file_create()、file_activate()、file_release(),以及mobile_create()、mobile_activate()和mobile_release()。
您可以编写一个处理对象生命周期的宏:
#define LIFECYCLE(name, func) (struct name x = name##_create(); name##_activate(x); func(x); name##_release())

当然,这种“最小化对象版本”的命名约定并不是唯一适用的命名约定 - 几乎绝大多数命名约定都利用了一个公共子串来形成名称。它可以是函数名称(如上所述),也可以是字段名称、变量名称或几乎任何其他内容。

2

我在C程序中使用它来帮助正确实施一组方法的原型,这些方法必须符合某种调用约定。从某种意义上说,这可以用于在纯C中实现简单的面向对象编程:

SCREEN_HANDLER( activeCall )

扩展后的内容可能类似于这样:

STATUS activeCall_constructor( HANDLE *pInst )
STATUS activeCall_eventHandler( HANDLE *pInst, TOKEN *pEvent );
STATUS activeCall_destructor( HANDLE *pInst );

当您执行以下操作时,这将强制对所有“派生”对象进行正确的参数化:

SCREEN_HANDLER( activeCall )
SCREEN_HANDLER( ringingCall )
SCREEN_HANDLER( heldCall )

在您的头文件等上述内容中,它对于维护也很有用,即使您想要更改定义和/或向“对象”添加方法。

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