带有一元减号的宏展开

13
请看下面的代码:

考虑以下代码:

#define A -100

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

现在,我测试了一些主要编译器(如MSVC、GCC、Clang),这些编译器都可以正常编译,bar == 100,这是因为所有编译器的预处理器在令牌之间插入了一个空格,所以你最终得到的代码是:

int bar = - -100;

由于我希望我的代码尽可能具有可移植性,因此我去检查了一下标准是否定义了这种行为,但我找不到任何相关内容。这种行为是由标准保证的,还是仅仅是编译器的特性?而且,像这样的naive方法(显然不会编译通过)bar = --100;是否也被允许?


7
如果你想要确保,可以使用 #define A (-100)。不过这并没有回答问题 :)。 - Zereges
@Zereges 是的,我知道几种解决方法。这是其中之一。但它在一个旧项目中被广泛使用,所以在改变它之前,我想确保它被很好地定义。 - Hatted Rooster
2
宏的一般规则是始终在表达式扩展周围放置括号,并在语句扩展周围放置花括号。 - SwiftMango
6
据我所知,如果没有 ##,你是无法将多个小片段组合成一个令牌的。 - chris
1
@chris 是正确的。像 -- 这样的多字符运算符需要使用标记粘贴才能从单独的标记中形成。 - L. Scott Johnson
1
只要你不试图支持80年代的一些预ANSI C预处理器... - Marc Glisse
2个回答

7
这在语言中有明确规定:两个-字符不会被连接成为一个--运算符。
这种不进行连接的方式是通过必须解析源文件的方式来保证的:宏展开是在第四个翻译阶段执行的。在此翻译阶段之前,在第三个翻译阶段期间,源文件必须转换为预处理token和空格序列[lex.phases]/3

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

因此,在第三个翻译阶段后,靠近bar定义附近的token序列可能如下所示:
// here {...,...,...} is used to list preprocessing tokens.
{int, ,bar, ,=, ,-,A,;}

然后在第4阶段之后,您将获得:

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

在第七阶段,理论上会移除空格:

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

5

一旦输入在翻译的早期阶段被分成了预处理标记(preprocessing tokens), 使两个相邻的预处理标记合并成一个标记的唯一方法是使用预处理器的 ## 运算符。这就是 ## 运算符的作用,也是它的必要性所在。

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

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

例如:

#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

那么,回答你的问题:是的,在标准兼容的实现中,这两个-字符永远不会变成--。但是插入空格的实际细节只是一个实现细节。其他一些实现可能会使用不同的技术来防止这些-合并成--


在实际的实现中,你能给一些例子吗?这样做是浪费的。 - Marc Glisse
1
@MarcGlisse Gcc?Clang?难道现在不是几乎所有人都这样做吗?(正如OP在他们的问题中所述) - AnT stands with Russia
不,至少对于gcc而言,在预处理和编译之间没有文本中间表示(如果你请求的话,gcc可以生成一个,但默认情况下不会)。 - Marc Glisse
@Marc Glisse:这是个很好的观点和有趣的问题。GCC文档仍然说cpp被用作预处理器,可能意味着这是一个单独的解耦模块。(当然,这并不一定意味着预处理结果必须被倒入中间文件)。但在现实中会发生什么呢?预处理器真的实现为一个独立的模块吗?如果是的话,它是如何与GCC中的编译器正常通信的呢?即使没有显式的预处理文件,他们仍然可以使用纯文本表示“即时执行”。他们这样做了吗? - AnT stands with Russia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接