##预处理程序运算符和陷阱需要考虑哪些应用?


87

正如我之前的许多问题中提到的那样,我正在研究K&R,目前正在使用预处理器。##预处理程序运算符是更有趣的事情之一,这是我以前学习C的任何尝试中从未知道的事情。根据K&R:

预处理程序运算符## 提供了一种在宏扩展过程中连接实际参数的方法。如果替换文本中##的参数与a相邻,则该参数将被实际参数替换, ##并删除空格和周围的空白,然后重新扫描结果。例如,宏paste 将其两个参数串联在一起:

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

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

有人将如何以及为什么在现实世界中使用它?使用它的实际例子是什么,是否有需要考虑的地方?

Answers:


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__);

他们将其与另一个宏一起使用,该宏返回带有日期和时间的字符串。

放在aL旁边__ DATE __会给您一个编译错误。


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 '16

49

使用令牌粘贴(' ##')或字符串化(' #')预处理运算符时要注意的一件事是,您必须使用额外的间接级别,以便它们在所有情况下都能正常工作。

如果您不这样做,并且传递给令牌粘贴操作符的项目本身就是宏,那么您将获得可能不是您想要的结果:

#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
对于这个预处理器行为的说明,请参阅stackoverflow.com/questions/8231966/...
亚当·戴维斯

@MichaelBurr我正在阅读您的答案,我对此表示怀疑。此LINE为何打印行号?
HEPL PLZ 2014年

3
@AbhimanyuAryan:我不确定这是否是您要的内容,但是它__LINE__是一个特殊的宏名称,该宏名称被预处理器替换为源文件的当前行号。
Michael Burr 2014年

如果可以引用/链接语言规范,那就太酷了,例如此处
Antonio

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
出于某种原因,它被称为“令牌粘贴”运算符-目的是在完成操作后以单个令牌结尾。不错的文章。
Mark Ransom

当令牌粘贴操作符的结果不是有效的预处理令牌时,则行为未定义。
alecov 2014年

语言更改(例如十六进制浮点数或(在C ++中)数字分隔符和用户定义的文字)会不断更改构成“有效预处理令牌”的内容,因此请不要那样滥用!如果您必须分离(适当的语言)标记,请将它们拼写为两个单独的标记,并且不要依赖预处理器语法和适当的语言之间的偶然交互。
Kerrek SB 2016年

6

这在各种情况下都很有用,以免不必要地重复自己。以下是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

有关堆栈溢出的上一个问题要求一种平滑的方法来生成枚举常量的字符串表示形式,而无需进行很多容易出错的重键入操作。

链接

我对这个问题的回答显示了如何应用少量预处理器魔术来让您定义这样的枚举(例如)……;

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

我在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 )

如果您碰巧想要更改定义和/或向“对象”添加方法,则对维护文件也很有用。


2

SGlib使用##从根本上捏造了C中的模板。由于没有函数重载,因此##用于将类型名称粘贴到生成的函数的名称中。如果我有一个名为list_t的列表类型,那么我将获得诸如sglib_list_t_concat之类的函数,依此类推。


2

我将它用于嵌入式非标准C编译器上的本地滚动断言:



#define ASSERT(exp) if(!(exp)){ \
                      print_to_rs232("Assert failed: " ## #exp );\
                      while(1){} //Let the watchdog kill us 



3
我认为您的意思是“非标准”,即编译器不执行字符串粘贴但执行令牌粘贴-否则即使没有也可以工作##吗?
PJTraill 2015年

1

我用它为宏定义的变量添加自定义前缀。所以像这样:

UNITTEST(test_name)

扩展为:

void __testframework_test_name ()

1

主要用途是当您具有命名约定并且您希望宏利用该命名约定时。也许您有几种方法系列: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())

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


1

WinCE中的一项重要用途:

#define BITFMASK(bit_position) (((1U << (bit_position ## _WIDTH)) - 1) << (bit_position ## _LEFTSHIFT))

在定义寄存器位描述时,我们执行以下操作:

#define ADDR_LEFTSHIFT                          0

#define ADDR_WIDTH                              7

在使用BITFMASK时,只需使用:

BITFMASK(ADDR)

0

这对于日志记录非常有用。你可以做:

#define LOG(msg) log_msg(__function__, ## msg)

或者,如果您的编译器不支持functionfunc

#define LOG(msg) log_msg(__file__, __line__, ## msg)

上面的“功能”记录消息,并确切显示哪个功能记录了消息。

我的C ++语法可能不太正确。


1
您打算怎么做?由于不需要将“,”标记粘贴到“ msg”,因此不用“ ##”也可以正常工作。您是否要对味精进行分类?另外,FILELINE必须大写,而不是小写。
bk1e

你确实是对的。我需要找到原始脚本以了解如何使用##。真可惜,今天没有饼干!
2008年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.