【问题标题】:Macro expansion with unary minus一元减号的宏扩展
【发布时间】:2019-10-07 03:38:06
【问题描述】:

考虑以下代码:

#define A -100

//later..
void Foo()
{
  int bar = -A;
  //etc..
}

现在,这在我测试过的一些主要编译器(MSVC、GCC、Clang)和bar == 100 上编译得很好,正如预期的那样,这是因为所有这些编译器的预处理器在标记之间插入了一个空格,所以你最终得到:

int bar = - -100;

由于我希望我的代码尽可能可移植,因此我检查了此行为是否由标准定义,但我找不到任何内容。标准是否保证了这种行为,或者这只是一个编译器功能,并且是否也允许使用幼稚的方法(显然不会编译)bar = --100;

【问题讨论】:

  • 如果你想确定,可以#define A (-100)。虽然没有回答问题:)
  • 宏的一般规则总是在表达式扩展周围放置括号,在语句扩展周围放置花括号
  • 据我所知,如果没有##,您将永远无法用较小的部分创建一个令牌。
  • @chris 是正确的。多字符运算符,例如 -- ,需要从单独的标记形成标记粘贴。
  • 只要您不尝试支持一些 80 年代的 pre-ANSI C 预处理器...

标签: c++ c-preprocessor c++03


【解决方案1】:

这是在语言中指定的:两个- 字符最终不会被连接形成-- 运算符。

必须通过解析源文件的方式来确保不存在连接:在翻译阶段 4 执行宏扩展。在此翻译阶段之前,在翻译阶段 3 期间,必须将源文件转换为一系列预处理标记和空格[lex.phases]/3:

源文件被分解为预处理标记和空白字符序列(包括 cmets)。源文件不应以部分预处理标记或部分注释结尾。13 每条注释由一个空格字符替换。保留换行符。未指定除换行符以外的每个非空空白字符序列是保留还是替换为一个空格字符。

所以在翻译阶段 3 之后,bar 定义附近的标记序列可能如下所示:

// here {...,...,...} is used to list preprocessing tokens.
{int, ,bar, ,=, ,-,A,;}

那么在第 4 阶段之后你会得到:

{int, ,bar, ,=, ,-,-, ,100,;}

在第 7 阶段从概念上移除空间:

{int,bar,=,-,-,100,;}

【讨论】:

    【解决方案2】:

    一旦在翻译的早期阶段将输入拆分为预处理标记,使两个相邻的预处理标记合并为一个标记的唯一方法是预处理器的## 运算符。这就是 ## 运算符的用途。这就是为什么它是必要的。

    一旦预处理完成,编译器将根据预解析的预处理标记来分析代码。编译器本身不会尝试将两个相邻的标记合并为一个标记。

    在您的示例中,内部 - 和外部 - 是两个不同的预处理标记。它们不会合并到一个 -- 令牌中,并且编译器不会将它们视为一个 -- 令牌。

    例如

    #define M1(a, b) a-b
    #define M2(a, b) a##-b
    
    int main()
    {
      int i = 0;
      int x = M1(-, i); // interpreted as `int x = -(-i);`
      int y = M2(-, i); // interpreted as `int y = --i;` 
    }
    

    这是语言规范定义行为的方式。

    在实际实现中,预处理阶段和编译阶段通常是相互分离的。并且预处理阶段的输出通常以纯文本形式表示(而不是某些令牌数据库)。在这样的实现中,预处理器和编译器必须就如何分离相邻(“接触”)预处理标记的一些约定达成一致。通常,预处理器会在源代码中碰巧“接触”的两个单独的标记之间插入一个额外的空格。

    该标准确实说明了有关该额外空间的任何内容,并且正式地说它不应该存在,但这正是这种分离通常在实践中实现的方式。

    请注意,由于该空间“不应该存在”,因此此类实现还必须做出一些努力以确保该额外空间在其他上下文中“不可检测”。例如

    #define M1(a, b) a-b
    #define M2(a, b) a##-b
    
    #define S_(x) #x
    #define S(x) S_(x)
    
    int main()
    {
      std::cout << S(M1(-, i)) << std::endl; // outputs `--i`
      std::cout << S(M2(-, i)) << std::endl; // outputs `--i`
    }
    

    main 的两行都应该输出--i

    所以,回答您最初的问题:是的,您的代码在某种意义上是可移植的,在符合标准的实现中,这两个- 字符永远不会变成--。但实际插入空间只是一个实现细节。其他一些实现可能会使用不同的技术来防止 - 合并到 --

    【讨论】:

    • “在实际实现中”你能举一些例子吗?这样做很浪费。
    • @MarcGlisse Gcc?铛?直到今天,几乎每个人都不是这样吗? (正如 OP 在他们的问题中所述)
    • 不,至少对于 gcc,预处理和编译之间没有文本中间表示(如果你问得好,gcc 可以生成一个,但默认情况下不会)。
    • @Marc Glisse:这是一个很好的观点和一个有趣的问题。 GCC 文档仍然说cpp 被用作预处理器,可能暗示这是一个单独的解耦模块。 (当然,这并不一定意味着必须将预处理结果转储到中间文件中)。但现实中会发生什么?预处理器真的是作为解耦模块实现的吗?如果是,它如何与 GCC 中的编译器进行通信?即使没有明确的预处理文件,他们仍然可以“即时”使用纯文本表示。是吗?
    猜你喜欢
    • 2016-05-14
    • 2021-07-17
    • 1970-01-01
    • 2021-05-16
    • 2018-01-03
    • 1970-01-01
    • 1970-01-01
    • 2017-06-29
    • 1970-01-01
    相关资源
    最近更新 更多