当条件为假时,为什么条件包含中的受控组在词汇上是有效的?

时间:2018-09-18 10:33:24

标签: c c-preprocessor conditional-compilation

以下程序进行编译:

// #define WILL_COMPILE 
#ifdef WILL_COMPILE
int i = 
#endif

int main()
{   
    return 0;
}

GCC实时演示here

但是以下内容将发出警告:

//#define WILL_NOT_COMPILE
#ifdef WILL_NOT_COMPILE
char* s = "failure
#endif

int main()
{   
    return 0;
}

GCC实时演示here

我知道在第一个示例中,在达到compilation phase of the translation时,已删除了受控组。因此它可以编译而不会出现错误或警告。

但是,当第二个示例不包含受控组时,为什么在词汇有效性上又要如此?

在线搜索,我发现了这个quote

  

即使条件失败,其内部的受控文本仍将通过初始转换和标记化来运行。因此,它必须在词法上都是有效的C。通常,唯一重要的方法是,失败的条件组中的所有注释和字符串文字都必须正确结束。

但这并没有说明为什么在条件失败时检查词法有效性。

我在这里错过了什么吗?

3 个答案:

答案 0 :(得分:2)

translation phase 3中,预处理器将生成预处理器令牌,并且将"终止于所有不能为上述字符之一的非空白字符  是未定义的行为。 参见C11 6.4 Lexical elements p3

  

令牌是翻译阶段7和8中语言的最小词汇元素。   标记的类别为:关键字,标识符,常量,字符串文字和标点符号。   预处理标记是翻译语言中最少的词汇元素   第3到第6阶段。预处理令牌的类别为:标头名称,   标识符,预处理数字,字符常量,字符串文字,标点符号和   在词法上与其他预处理不匹配的单个非空白字符   令牌类别。69)如果'或“字符与最后一个类别匹配,则行为为   未定义。 ....

preprocessing-token供参考:

  

预处理令牌:
                   标头名称
                   标识符
                   pp-编号
                   字符常量
                   字串文字
                   标点符号
                   每个非空格字符都不能为上述字符之一

第二个示例中不匹配的"non-white-space character that cannot be one of the above匹配。

由于这是未定义的行为,而不是约束,因此编译器没有义务对其进行诊断,但是可以肯定的是,编译器可以使用-pedantic-errors对其进行诊断,甚至会变成错误godbolt session。正如rici所指出的,只有当令牌在预处理中幸存下来时,它才会成为违反约束的行为。

gcc document you cite基本上说的是同一句话:

  

...即使条件失败,其内部的受控文本也会仍通过初始转换和令牌化运行。因此,它必须在词法上都是有效的C。通常,唯一重要的方法是,失败的条件组中的所有注释和字符串文字都必须正确结束。 ...

答案 1 :(得分:1)

“为什么[关于C的东西]是这样?”通常无法回答问题,因为编写1989 C标准的人都没有在这里回答问题(据我所知),如果在这里是 ,则将近30岁年前,他们可能不记得了。

但是,我可以想到一个合理的原因,为什么要求跳过的条件组的内容必须包含有效的预处理令牌序列。请注意,不需要包含有效的预处理令牌序列的注释:

/* this comment's perfectly fine even though it has an unclosed
   character literal inside */

还请注意,扫描注释的末尾非常简单。 /*寻找下一个*///寻找该行的结尾。唯一的麻烦是,应首先转换三字母组合和反斜杠换行符。标记注释内容将是多余的代码,没有用。

相比之下,扫描已跳过的条件组的末尾并不容易,因为条件组会嵌套。您必须寻找#if#ifdef#ifndef以及#else#endif,然后计算深度。而且所有这些指令都是根据预处理器标记进行词法定义的,因为当您不在跳过的条件组中时,这是最自然的查找方式。要求将跳过的条件组设为可标记化,可以使预处理程序使用与跳过其他条件组相同的代码来处理跳过的条件组中的指令。

默认情况下,当GCC在跳过的条件组内遇到无法标记的行时,它仅发出警告,而在其他地方则为错误:

#if 0
"foo
#endif
"bar

给我

test.c:2:1: warning: missing terminating " character
"foo
^
test.c:4:1: error: missing terminating " character
"bar
^~~~

这是一个故意的宽大处理,可能是我自我介绍的(自从我写了GCC当前预处理程序的三分之一以来已经二十多年了,但是我仍然忘记了很多细节)。您会看到,原始 C预处理器,一个K和R写道, did 允许在跳过的条件组内进行任意废话,因为它不是围绕令牌中的概念构建的第一名;它将文本转换为其他文本。因此,人们会在#if 0#endif之间放置评论而不是/**/,并且这些评论有时自然会包含撇号。因此,当我和Per Bothner和Neil Booth以及Chiaki Ishikawa一起用集成的,完全符合标准的“ cpplib”(大约GCC 3.0)替换了GCC的原始“ C兼容编译器预处理器” 1 时,我们感到我们需要在此处减少一些兼容性松弛。


1 如果您的年龄足够大,请举手,以了解RMS为什么认为这个名字很有趣。

答案 2 :(得分:1)

翻译阶段3 (C11 5.1.1.2/3)的描述,发生在执行预处理指令之前:

  

源文件被分解为预处理令牌和序列   空格字符(包括注释)。

预处理令牌的语法为:

  

标题名称
  标识符
   pp-number
  恒定字符
   string-literal
  标点符号
  每个非空格字符都不能为上述字符之一

尤其要注意, string-literal 是单个预处理令牌。后续说明(C11 6.4 / 3)阐明:

  

如果'"字符与最后一个类别匹配,则行为是   未定义。

因此您的第二个代码在翻译阶段3导致未定义的行为。