ChatGPT解决这个技术问题 Extra ChatGPT

## 预处理器运算符的应用和要考虑的问题是什么?

正如我之前的许多问题中提到的,我正在通过 K&R 工作,目前正在进入预处理器。更有趣的事情之一——我以前尝试学习 C 时从未知道的事情——是 ## 预处理器运算符。根据 K&R:

预处理器运算符## 提供了一种在宏扩展期间连接实际参数的方法。如果替换文本中的参数与## 相邻,则将参数替换为实际参数,删除## 和周围的空白,并重新扫描结果。例如,宏 paste 连接它的两个参数: #define paste(front, back) front ## back 所以 paste(name, 1) 创建标记 name1。

有人如何以及为什么会在现实世界中使用它?有哪些实际使用示例,是否有需要考虑的问题?


M
Michael Burr

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

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

#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

有关此预处理器行为的说明,请参阅 stackoverflow.com/questions/8231966/…
@MichaelBurr 我正在阅读您的答案,我有疑问。这条 LINE 怎么会打印行号?
@AbhimanyuAryan:我不确定这是否是您要问的,但 __LINE__ 是一个特殊的宏名称,它被预处理器替换为源文件的当前行号。
如果可以引用/链接语言规范,那就太好了,如 here
P
Peter Mortensen

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;

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

这是我在升级到新版本的编译器时遇到的一个问题:

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

当令牌粘贴运算符的结果不是有效的预处理器令牌时,令牌粘贴运算符是不必要的并且可能有害。

例如,可以尝试在编译时使用标记粘贴运算符构建字符串文字:

#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 包含有关令牌粘贴运算符的更多有用信息。


谢谢 - 我不知道这一点(但后来我并没有过多地使用这些预处理运算符......)。
出于某种原因,它被称为“令牌粘贴”运算符 - 目的是在您完成后以单个令牌结束。不错的文案。
当令牌粘贴运算符的结果不是有效的预处理器令牌时,行为未定义。
十六进制浮点数或(在 C++ 中)数字分隔符和用户定义的文字等语言更改会不断更改构成“有效预处理令牌”的内容,因此请不要那样滥用它!如果您必须分开(语言本身)标记,请将它们拼写为两个单独的标记,并且不要依赖预处理器语法和语言本身之间的意外交互。
V
Vebjorn Ljosa

这在各种情况下都很有用,以免不必要地重复自己。以下是 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"(并且可能拼写错误)。


C
Community

Stack Overflow 上的前一个问题要求一种平滑的方法来生成枚举常量的字符串表示,而无需大量容易出错的重新输入。

Link

我对这个问题的回答表明,应用小的预处理器魔法如何让你像这样定义你的枚举(例如)......;

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

... 宏扩展不仅定义了枚举(在 .h 文件中),而且还定义了匹配的字符串数组(在 .c 文件中);

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

字符串表的名称来自使用## 运算符将宏参数(即颜色)粘贴到StringTable。像这样的应用程序(技巧?)是 # 和 ## 运算符非常宝贵的地方。


P
Peter Mortensen

当您需要将宏参数与其他内容连接时,您可以使用令牌粘贴。

它可用于模板:

#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;
};

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


T
Tall Jeff

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

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



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


我认为您所说的“非标准”是指编译器没有进行字符串粘贴但确实进行了标记粘贴——或者即使没有 ## 它也能工作?
J
John Millikin

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

UNITTEST(test_name)

扩展为:

void __testframework_test_name ()

m
mcherm

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

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


K
Keshava GN

WinCE 中的一项重要用途:

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

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

#define ADDR_LEFTSHIFT                          0

#define ADDR_WIDTH                              7

在使用 BITFMASK 时,只需使用:

BITFMASK(ADDR)

y
ya23

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

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

或者,如果您的编译器不支持函数和函数:

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

上面的“函数”记录消息并准确显示哪个函数记录了一条消息。

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


你想做什么?如果没有“##”,它也可以正常工作,因为不需要将“,”标记粘贴到“msg”。您是否尝试对味精进行字符串化?此外,FILE 和 LINE 必须是大写,而不是小写。
你确实是对的。我需要找到原始脚本以查看## 是如何使用的。真可惜,今天没有饼干!