【问题标题】:What are the applications of the ## preprocessor operator and gotchas to consider?## 预处理器运算符的应用和要考虑的问题是什么?
【发布时间】:2010-09-18 00:41:10
【问题描述】:

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

预处理运算符## 提供了一种连接实际的方法 宏扩展期间的参数。如果一个 替换文本中的参数是 与## 相邻,参数为 替换为实际参数, ## 和周围的空白是 删除,并重新扫描结果。 例如,宏 paste 连接它的两个参数:

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

所以paste(name, 1) 创建了令牌 name1.

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

【问题讨论】:

    标签: c c-preprocessor kernighan-and-ritchie stringification


    【解决方案1】:

    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__); 并一次隐式构建整个字符串。
    【解决方案2】:

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

    UNITTEST(test_name)
    

    扩展到:

    void __testframework_test_name ()
    

    【讨论】:

      【解决方案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;
      };
      

      同样可以写一个函数模板进行列表遍历。

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          【解决方案5】:

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

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

          【讨论】:

            【解决方案6】:

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

            在您的头文件等中。如果您甚至想更改定义和/或向“对象”添加方法,这对于维护也很有用。

            【讨论】:

              【解决方案7】:

              SGlib 使用 ## 基本上是在 C 中伪造模板。因为没有函数重载,所以 ## 用于将类型名称粘合到生成的函数的名称中。如果我有一个名为 list_t 的列表类型,那么我会得到名为 sglib_list_t_concat 的函数,等等。

              【讨论】:

                【解决方案8】:

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

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

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

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

                上述“函数”记录消息并准确显示哪个函数记录了消息。

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

                【讨论】:

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

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

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

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

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

                #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++ 中)数字分隔符和用户定义的文字,会不断改变构成“有效预处理令牌”的内容,因此请切勿滥用它像那样!如果您必须分开(语言专有)标记,请将它们拼写为两个单独的标记,并且不要依赖预处理器语法和语言专有之间的意外交互。
                【解决方案10】:

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

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

                【讨论】:

                • 我认为你所说的“非标准”是指编译器没有进行字符串粘贴但确实进行了标记粘贴——或者即使没有## 它也能工作?
                【解决方案11】:

                关于 StackOverflow 的上一个问题询问了一种为枚举常量生成字符串表示的平滑方法,而无需大量容易出错的重新输入。

                Link

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

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

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

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

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

                【讨论】:

                  【解决方案12】:

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

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

                  #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
                  【解决方案13】:

                  WinCE 中的一个重要用途:

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

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

                  #define ADDR_LEFTSHIFT                          0
                  
                  #define ADDR_WIDTH                              7
                  

                  在使用 BITFMASK 时,只需使用:

                  BITFMASK(ADDR)
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-08-11
                    • 2018-06-09
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-05-30
                    相关资源
                    最近更新 更多