为什么 C 预处理器是未定义行为的主题?

Why is the C preprocessor a subject of undefined behavior?

我能理解:

但是,我还不能理解为什么 C 预处理器是未定义行为的主题?众所周知,预处理指令是在编译时执行的。

考虑 C11,6.10.3.3 ## 运算符,3:

If the result is not a valid preprocessing token, the behavior is undefined.

为什么不把它作为一个约束呢?例如:

The result shall be a valid preprocessing token.

同样的问题适用于 6.10 预处理指令中的所有其他“行为未定义”。

不谈具体细节,我的猜测是,存在一些存在错误的预处理器实现,但出于兼容性原因,标准不想声明它们不符合要求。

用人类语言来说:如果你写的程序里面有 X,预处理器会做一些奇怪的事情。

在标准语中:带有 X 的程序的行为是未定义的。

如果标准说“结果应是有效的预处理令牌”之类的话,可能不清楚“应”在此上下文中的含义。

  • 程序员写程序应该满足这个条件?如果是的话,“undefined behavior”的写法就更加清晰统一了(其他地方也出现了)
  • 预处理器应确保此条件成立?如果是这样,这需要专门的逻辑来检查条件;实施起来可能不切实际。

Why is the C preprocessor a subject of undefined behavior?

在创建C标准的时候,有一些现成的C预处理器,也有一些标准化委员会成员心中想象的理想C预处理器。

所以存在这些灰色区域,委员会成员不完全确定他们想要做什么and/or现有的 C 预处理器实现在行为上彼此不同。

因此,这些情况不是定义的行为。因为 C 委员会成员并不完全确定行为实际上应该是什么。所以没有要求它应该是什么。

One of the origins of the UB

是的,其中一个

UB 可能存在以简化语言的实现。例如,在预处理器的情况下,预处理器作者 不必关心 当无效的预处理器标记是 ##.[=14 的结果时会发生什么=]

或者 UB 可能存在以协调具有不同行为的现有实现或作为扩展点。因此,在 UB 情况下出现段错误的预处理器,在 UB 情况下接受并工作的预处理器,以及在 UB 情况下格式化硬盘驱动器的预处理器,都可以符合标准(但我不想工作 that one that formats your drive).

假设通过 include 指令读入的文件以部分行结尾:

#define foo bar

根据预处理器的设计,部分标记 bar 可能会连接到出现在 #include 指令之后的行开头的任何内容,或者出现在该行的行为就好像它被放置在带有 #define 指令的行上,但是用一个空格将它与标记 bar 分隔开,并且很难想象构建脚本可能依赖于这样的行为。实现也可能表现得好像在包含文件的末尾插入了换行符,或者可能忽略此类文件的最后部分行。

任何依赖于前一种行为的代码显然是不可移植的,但是如果代码利用这种行为来做一些原本不实用的事情,这样的代码几乎不会是“错误的”,并且标准的作者不会想禁止可以有效处理它的实现继续这样做。

当标准使用短语“不可移植或错误”时,这并不意味着“不可移植,因此是错误的”。在 C89 发布之前,C 实现定义了许多有用的结构,但其中 none 是由“C 标准”定义的,因为没有一个。如果一个实现定义了某些构造的行为,而另一些则没有,并且标准将构造保留为“未定义”,那只会保持现状,即选择定义有用行为的实现会这样做,而那些选择不定义的实现不会,依赖于此类行为的程序将是“不可移植的”,可以在支持这些行为的实现上正常工作,但不能在不支持这些行为的实现上正常工作。