我正在基于GCC编写自己的C预处理器。到目前为止,它几乎是相同的,但我认为多余的是对通过##
连接的令牌执行任何形式的检查。因此,在我的预处理器手册中,我写了这样的内容:
3.5级联
GCC禁止使用两个互不兼容的预处理进行串联诸如";x〃;以及"+"(按任何顺序)。这将导致以下错误:";粘贴";x〃;以及"+"没有给出有效的预处理令牌";然而,对于这个预处理器-串联来说,这不是真的可能发生在任何令牌之间。
我的推理很简单:如果它扩展到无效代码,那么编译器将产生错误,因此我不必显式处理这种情况,这会使预处理器速度变慢,并增加代码复杂性。如果它产生了有效的代码,那么这种限制的删除只会使它更加灵活(尽管可能在极少数情况下)。
所以我想问,为什么会出现这种错误,为什么会实际应用这种限制,如果我在预处理器中忽略它,这真的是犯罪吗?
就ISO C而言,如果##
创建了一个无效的令牌,则行为是未定义的。但这里有一种奇怪的情况,如下所示。C的预处理-翻译阶段的输出是预处理令牌(pp令牌)流。这些被转换为标记,然后进行语法和语义分析。现在有一条重要的规则:如果pp令牌没有一个可以将其转换为令牌的形式,那就是违反了约束。因此,您在没有##
运算符帮助的情况下自行编写的垃圾预处理器令牌必须被诊断为词汇语法错误。但是,如果使用##
创建一个错误的预处理令牌,则行为是未定义的。
注意其中的微妙之处:如果##
用于创建一个糟糕的预处理令牌,那么它的行为是未定义的。粘贴并不是定义明确的,然后在pp令牌转换为令牌的阶段被捕获:从##
的评估点开始,它就没有定义。
基本上,这是历史性的。C预处理器在历史上(可能有些是)是独立的程序,其词法分析与下游编译器不同,也更松散。C标准试图以某种方式从一种具有翻译阶段的语言的角度来捕捉这一点,结果有一些怪癖和可能令人惊讶的地方。(例如,在预处理翻译阶段,数字标记("pp number")是一种奇怪的词汇语法,它允许胡言乱语,例如具有多个浮点E
指数的标记。)
现在,回到你的处境。您的文本C预处理器实际上并不输出pp令牌对象;它输出另一个文本流。您可能在内部拥有pp令牌对象,但它们在输出时会变平。因此,您可能会想,为什么不允许您的##
操作符盲目地将任意两个令牌粘合在一起呢?净效果是,这些令牌被转储到输出流中,而没有任何空白。(这可能就是早期支持##
并作为单独程序运行的预处理器的全部内容)。
不幸的是,这意味着您的##
操作符不是纯粹的真正的标记粘贴操作符;它只是一个盲并置运算符,当它碰巧并置两个令牌时,有时会产生一个令牌,下游编译器会将其作为一个令牌进行词法分析。如果你这样做,最好诚实地将其记录下来,而不是将其描述为一种灵活性功能。
另一方面,在##
操作符中拒绝坏的预处理令牌的一个很好的理由是捕捉它无法实现其记录的工作描述的情况:从两个令牌中提取一个令牌的要求。其思想是,程序员知道语言规范(程序员和实现之间的合同),知道##
应该制作一个令牌,并依赖于此。对于这样的程序员来说,任何涉及错误的令牌粘贴的情况都是错误的,并且该程序员最好通过诊断得到支持。
GCC和GNUCPP预处理器的维护者可能持有这样的观点:预处理器不是一个灵活的文本处理工具,而是支持有纪律的C编程的工具链的一部分。
此外,糟糕的令牌粘贴作业的未定义行为很容易被诊断出来,为什么不进行诊断呢标准中没有这方面的诊断要求,这看起来只是一个历史性的让步。这是一种";低挂水果";诊断。让那些未定义的行为得不到诊断,因为诊断是困难或棘手的,或者需要运行时惩罚。