一元减的宏扩展

时间:2019-05-20 18:40:56

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

考虑以下代码:

#define A -100

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

现在,这可以在我测试的某些主要编译器(MSVC,GCC,Clang)和bar == 100上正常编译,这是因为所有这些编译器的预处理器在标记之间插入了一个空格,所以最终:

int bar = - -100;

由于我希望代码尽可能地可移植,因此我去检查此行为是否由标准定义,但是我找不到任何内容。这种行为是由标准保证的还是仅仅是编译器功能,是否也允许bar = --100;的天真的方法(显然不会编译)?

2 个答案:

答案 0 :(得分:6)

这是用以下语言指定的:两个-字符将不会以串联形式组成--运算符。

可以通过解析源文件的方式来确保不存在串联:在转换阶段4中执行宏扩展。在此转换阶段之前,在转换阶段3中,必须按一系列预处理令牌和顺序对源文件进行转换。空白[lex.phases]/3

  

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

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

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

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

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

从概念上讲,在阶段7中删除了空间:

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

答案 1 :(得分:5)

在翻译的早期阶段,一旦将输入分为个预处理令牌,唯一的使两个相邻的预处理令牌合并为一个令牌的方法就是##预处理程序运算符。这就是##运算符的作用。这就是为什么有必要的原因。

一旦预处理完成,适当的编译器将根据预先解析的预处理令牌分析代码。适当的编译器不会尝试将两个相邻的令牌合并为一个令牌。

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

例如

#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

因此,要回答您的原始问题:是的,您的代码在某种意义上是可移植的,即在符合标准的实现中,这两个-字符将永远不会成为--。但是实际的空间插入只是实现细节。其他一些实现可能使用其他技术来防止那些-合并为--