正如我之前的许多问题中提到的,我正在通过 K&R 工作,目前正在进入预处理器。更有趣的事情之一——我以前尝试学习 C 时从未知道的事情——是 ##
预处理器运算符。根据 K&R:
预处理器运算符## 提供了一种在宏扩展期间连接实际参数的方法。如果替换文本中的参数与## 相邻,则将参数替换为实际参数,删除## 和周围的空白,并重新扫描结果。例如,宏 paste 连接它的两个参数: #define paste(front, back) front ## back 所以 paste(name, 1) 创建标记 name1。
有人如何以及为什么会在现实世界中使用它?有哪些实际使用示例,是否有需要考虑的问题?
使用标记粘贴 ('##
') 或字符串化 ('#
') 预处理运算符时要注意的一件事是,您必须使用额外的间接级别才能使它们正常工作所有情况。
如果您不这样做并且传递给令牌粘贴运算符的项目本身就是宏,您将获得可能不是您想要的结果:
#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
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__);
他们将它与另一个宏一起使用,该宏返回带有日期和时间的字符串。
将 L
放在 __ 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;
std::wstring BuildDate = WIDEN(__DATE__) L" " WIDEN(__TIME__);
并一次隐式构建整个字符串。
这是我在升级到新版本的编译器时遇到的一个问题:
不必要地使用标记粘贴运算符 (##
) 是不可移植的,并且可能会产生不需要的空格、警告或错误。
当令牌粘贴运算符的结果不是有效的预处理器令牌时,令牌粘贴运算符是不必要的并且可能有害。
例如,可以尝试在编译时使用标记粘贴运算符构建字符串文字:
#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 documentation chapter on concatenation 包含有关令牌粘贴运算符的更多有用信息。
这在各种情况下都很有用,以免不必要地重复自己。以下是 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"
(并且可能拼写错误)。
Stack Overflow 上的前一个问题要求一种平滑的方法来生成枚举常量的字符串表示,而无需大量容易出错的重新输入。
我对这个问题的回答表明,应用小的预处理器魔法如何让你像这样定义你的枚举(例如)......;
ENUM_BEGIN( Color )
ENUM(RED),
ENUM(GREEN),
ENUM(BLUE)
ENUM_END( Color )
... 宏扩展不仅定义了枚举(在 .h 文件中),而且还定义了匹配的字符串数组(在 .c 文件中);
const char *ColorStringTable[] =
{
"RED",
"GREEN",
"BLUE"
};
字符串表的名称来自使用## 运算符将宏参数(即颜色)粘贴到StringTable。像这样的应用程序(技巧?)是 # 和 ## 运算符非常宝贵的地方。
当您需要将宏参数与其他内容连接时,您可以使用令牌粘贴。
它可用于模板:
#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;
};
同样,您可以编写一个用于列表遍历的函数模板。
我在 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 )
以上在您的头文件等中。如果您甚至碰巧想要更改定义和/或向“对象”添加方法,这对于维护也很有用。
SGlib 使用 ## 基本上是在 C 中伪造模板。因为没有函数重载,所以 ## 用于将类型名称粘合到生成的函数的名称中。如果我有一个名为 list_t 的列表类型,那么我会得到名为 sglib_list_t_concat 的函数,依此类推。
我将它用于嵌入式非标准 C 编译器上的家庭滚动断言:
#define ASSERT(exp) if(!(exp)){ \
print_to_rs232("Assert failed: " ## #exp );\
while(1){} //Let the watchdog kill us
##
它也能工作?
我用它来为宏定义的变量添加自定义前缀。所以像:
UNITTEST(test_name)
扩展为:
void __testframework_test_name ()
主要用途是当您有一个命名约定并且您希望您的宏利用该命名约定时。也许您有几个方法系列: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())
当然,一种“最小版本的对象”并不是唯一适用的命名约定——几乎绝大多数命名约定都使用公共子字符串来形成名称。它可以是函数名(如上)、字段名、变量名或其他任何东西。
WinCE 中的一项重要用途:
#define BITFMASK(bit_position) (((1U << (bit_position ## _WIDTH)) - 1) << (bit_position ## _LEFTSHIFT))
在定义寄存器位描述时,我们执行以下操作:
#define ADDR_LEFTSHIFT 0
#define ADDR_WIDTH 7
在使用 BITFMASK 时,只需使用:
BITFMASK(ADDR)
这对于日志记录非常有用。你可以做:
#define LOG(msg) log_msg(__function__, ## msg)
或者,如果您的编译器不支持函数和函数:
#define LOG(msg) log_msg(__file__, __line__, ## msg)
上面的“函数”记录消息并准确显示哪个函数记录了一条消息。
我的 C++ 语法可能不太正确。
__LINE__
是一个特殊的宏名称,它被预处理器替换为源文件的当前行号。