一元减号的宏扩展
Macro expansion with unary minus
考虑以下代码:
#define A -100
//later..
void Foo()
{
int bar = -A;
//etc..
}
现在,这在我测试的一些主要编译器(MSVC、GCC、Clang)和 bar == 100
上编译正常,这是因为所有这些编译器的预处理器在令牌所以你最终得到:
int bar = - -100;
因为我希望我的代码尽可能具有可移植性,所以我去检查了此行为是否由标准定义,但我在其中找不到任何内容。这种行为是否由标准保证,或者这只是一个编译器功能,是天真的方法(显然不会编译)bar = --100;
也允许吗?
这在语言中有规定:两个-
字符不会end-up连接成一个--
运算符。
必须解析源文件的方式确保没有串联:在翻译阶段 4 中执行宏扩展。在此翻译阶段之前,在翻译阶段 3 中,必须在一系列预处理中转换源文件标记和空格 [lex.phases]/3:
The source file is decomposed into preprocessing tokens and sequences of white-space characters (including comments). A source file shall not end in a partial preprocessing token or in a partial comment.13 Each comment is replaced by one space character. New-line characters are retained. Whether each nonempty sequence of white-space characters other than new-line is retained or replaced by one space character is unspecified.
因此在翻译阶段 3 之后,bar 定义附近的标记序列可能如下所示:
// here {...,...,...} is used to list preprocessing tokens.
{int, ,bar, ,=, ,-,A,;}
然后在第 4 阶段后您将获得:
{int, ,bar, ,=, ,-,-, ,100,;}
Space 在第 7 阶段在概念上被删除:
{int,bar,=,-,-,100,;}
一旦在翻译的早期阶段将输入拆分为 预处理标记 ,使两个相邻的预处理标记合并为一个标记的唯一方法是 ##
运算符预处理器。这就是 ##
运算符的用途。这就是为什么它是必要的。
预处理完成后,编译器将根据 pre-parsed 预处理标记分析代码。编译器本身不会尝试将两个相邻的标记合并为一个标记。
在您的示例中,内部 -
和外部 -
是两个不同的预处理标记。它们不会合并为一个 --
标记,并且编译器不会将它们视为一个 --
标记。
例如
#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;`
}
这是语言规范定义行为的方式。
在实际实现中,预处理阶段和编译阶段通常是相互解耦的。预处理阶段的输出通常以纯文本形式表示(而不是某些令牌数据库)。在这样的实现中,预处理器和编译器本身必须就如何分隔相邻 ("touching") 预处理标记的一些约定达成一致。通常,预处理器会在源代码中 "touch" 发生的两个单独标记之间插入一个额外的 space 。
标准确实对那个额外的 space 做了任何说明,并且正式地说它不应该在那里,但这正是这种分离在实践中通常是如何实现的。
请注意,由于 space "is not supposed to be there",此类实现还必须做出一些努力以确保此额外的 space 在其他情况下为 "undetectable"。例如
#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
.
因此,回答您最初的问题:是的,从某种意义上说,您的代码是可移植的,在 standard-compliant 实现中,这两个 -
字符永远不会变成 --
。但是 space 的实际插入只是一个实现细节。其他一些实现可能会使用不同的技术来防止 -
合并到 --
.
考虑以下代码:
#define A -100
//later..
void Foo()
{
int bar = -A;
//etc..
}
现在,这在我测试的一些主要编译器(MSVC、GCC、Clang)和 bar == 100
上编译正常,这是因为所有这些编译器的预处理器在令牌所以你最终得到:
int bar = - -100;
因为我希望我的代码尽可能具有可移植性,所以我去检查了此行为是否由标准定义,但我在其中找不到任何内容。这种行为是否由标准保证,或者这只是一个编译器功能,是天真的方法(显然不会编译)bar = --100;
也允许吗?
这在语言中有规定:两个-
字符不会end-up连接成一个--
运算符。
必须解析源文件的方式确保没有串联:在翻译阶段 4 中执行宏扩展。在此翻译阶段之前,在翻译阶段 3 中,必须在一系列预处理中转换源文件标记和空格 [lex.phases]/3:
The source file is decomposed into preprocessing tokens and sequences of white-space characters (including comments). A source file shall not end in a partial preprocessing token or in a partial comment.13 Each comment is replaced by one space character. New-line characters are retained. Whether each nonempty sequence of white-space characters other than new-line is retained or replaced by one space character is unspecified.
因此在翻译阶段 3 之后,bar 定义附近的标记序列可能如下所示:
// here {...,...,...} is used to list preprocessing tokens.
{int, ,bar, ,=, ,-,A,;}
然后在第 4 阶段后您将获得:
{int, ,bar, ,=, ,-,-, ,100,;}
Space 在第 7 阶段在概念上被删除:
{int,bar,=,-,-,100,;}
一旦在翻译的早期阶段将输入拆分为 预处理标记 ,使两个相邻的预处理标记合并为一个标记的唯一方法是 ##
运算符预处理器。这就是 ##
运算符的用途。这就是为什么它是必要的。
预处理完成后,编译器将根据 pre-parsed 预处理标记分析代码。编译器本身不会尝试将两个相邻的标记合并为一个标记。
在您的示例中,内部 -
和外部 -
是两个不同的预处理标记。它们不会合并为一个 --
标记,并且编译器不会将它们视为一个 --
标记。
例如
#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;`
}
这是语言规范定义行为的方式。
在实际实现中,预处理阶段和编译阶段通常是相互解耦的。预处理阶段的输出通常以纯文本形式表示(而不是某些令牌数据库)。在这样的实现中,预处理器和编译器本身必须就如何分隔相邻 ("touching") 预处理标记的一些约定达成一致。通常,预处理器会在源代码中 "touch" 发生的两个单独标记之间插入一个额外的 space 。
标准确实对那个额外的 space 做了任何说明,并且正式地说它不应该在那里,但这正是这种分离在实践中通常是如何实现的。
请注意,由于 space "is not supposed to be there",此类实现还必须做出一些努力以确保此额外的 space 在其他情况下为 "undetectable"。例如
#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
.
因此,回答您最初的问题:是的,从某种意义上说,您的代码是可移植的,在 standard-compliant 实现中,这两个 -
字符永远不会变成 --
。但是 space 的实际插入只是一个实现细节。其他一些实现可能会使用不同的技术来防止 -
合并到 --
.