为什么C++编译器会在行后报错而不是在其上?

10

今天在工作时,我和编译器又一次陷入了家务事之中,这个问题突然浮现在我的脑海中。尽管我因为在工作中不断按分号而得到了坚硬的小指,但我仍然在一个if语句之前忘记了加上一个分号。显然,这导致了编译错误:

error C2143: syntax error : missing ';' before 'if'

于是我想:“那么,为什么你不能告诉我缺少分号的那一行呢?而非在问题之后再提示我。”接着我尝试着对其他类似的语法错误进行实验:

error C2065: 'myUndeclared' : undeclared identifier

error C2143: syntax error : missing ')' before 'if'

等等...

所有这些错误都会像之前那样把我带到问题之后的那一行,并抱怨if语句之前的某些东西。

考虑以下情况:

SomeFunction(x) //Notice, there is no ';' here

if(bSomeCondition)
{
    ...
}

我遇到两个编译错误:

(第265行) error C2065: 'x' : 未声明的标识符

(第266行) error C2143: 语法错误 : 缺少“;”(在“if”之前)

然而,第一个错误正确告诉了我行号,尽管缺少分号。这使我认为编译器在解析时没有被绊倒,能够通过分号问题。那么为什么编译器坚持以这种“before”的方式报告语法错误呢?其他错误(非语法错误)报告的是它们所在的行。这是否与编译器进行多次通行有关?基本上,我希望有C++编译器工作知识的人可以具体解释编译器正在做什么,需要按照这种“before”方式报告错误。


1
你尝试在带有“if”的那一行添加分号,即;if了吗? - Jonas Heidelberg
9个回答

24
“为什么C/C++的错误提示这么烂”这个更普遍的问题的简短回答是“有时候C++很难解析”(它实际上没有上下文无关文法)。然而,这不是真正的原因 - 人们仍然可以制作比大多数C++编译器记录更好的诊断信息的工具。
更实际的答案是“编译器作者继承了不重视错误消息的传统代码库”,加上一点“编译器作者很懒”,再加上“诊断报告不是一个令人兴奋的问题”。大多数编译器作者会增加新的语言特性或3%的代码生成性能提升,而不是对代码库进行重构以允许良好的错误报告。关于“为什么错误没有被正确地定位到引起它们的那一行”的具体问题就是一个例子。实际上,编译器通常可以弄清楚缺少一个分号,并告诉您最后一个缺失语句的源跨度——即使存在C++的一般空格不变性。只是存储这些信息在历史上(在很大程度上)被忽略了。
话虽如此,没有被几十年旧代码所限制的新编译器做得更好。看看以合理的错误消息自豪的Clang编译器,它们比GCC好得多。这个诊断页面为这种情况提供了一个例子:
  $ gcc-4.2 t.c
  t.c: In function 'foo':
  t.c:5: error: expected ';' before '}' token
  $ clang t.c
  t.c:4:8: error: expected ';' after expression
    bar()
         ^
         ;

或者,更令人印象深刻的是:

  $ cat t.cc
  template<class T>
  class a {}
  class temp {};
  a<temp> b;
  struct b {
  }
  $ gcc-4.2 t.cc
  t.cc:3: error: multiple types in one declaration
  t.cc:4: error: non-template type 'a' used as a template
  t.cc:4: error: invalid type in declaration before ';' token
  t.cc:6: error: expected unqualified-id at end of input
  $ clang t.cc
  t.cc:2:11: error: expected ';' after class
  class a {}
            ^
            ;
  t.cc:6:2: error: expected ';' after struct
  }
   ^
   ;

看,它甚至告诉我们在哪里输入来解决问题!</clang_salespitch>


4
为什么?我们可以清楚地看到,C++的空格不变性引起行报告问题没有最终的技术原因。这只是一种编程选择,主要是由于传统的C++解析器所迫。Clang解析器可以很好地处理提问者的情况。 - Adam Wright
1
@Chad:如果你认为只是一点努力,那是因为你从未需要维护一个庞大(遗留)的代码库。 - Karoly Horvath
1
关于这个问题,它会说什么:http://ideone.com/XxoKM?它是否也期望有一个分号?我的观点是,虽然有一些常见的错误和常见的原因,但在一般情况下,如果给出了错误的语法,你就无法知道应该如何修复它,除非知道代码的预期功能。此外,我认为这是编译器消息糟糕的更重要原因,而不是语言难以解析。在我看来,有一些完全简单明了的语言,其错误消息也很糟糕。 - UncleBens
@UncleBens 我认为这是一个非常有价值的观点。显然,真正的“错误”是缺少了一个+运算符。我认为让编译器尝试推断这种情况将是一个有趣的挑战。我并不是说这很容易,但这绝对是一个值得解决的问题。 - Chad La Guardia
对于使用模板的代码进行调试,GCC的错误消息几乎没有用。它们通常甚至不会告诉您发生错误的代码行,只是在该文件中的某个位置。非常高兴发现CLang。 - orodbhen
显示剩余4条评论

12

因为在C++中,空白字符并不重要。所以这段代码是有效的:

SomeFunction(x)

;if(bSomeCondition)
{
    ...
}

所以编译器报错只是在提醒在if之前可能缺少了分号。


是的,但编译器肯定可以确定缺少的分号必须在“第1行”和“第3行”之间,而不是“仅仅在第3行之前的某个地方”。 - Inverse

6
在这段代码中:
SomeFunction(x)
if (y) {
}

正如你所说,错误会报告在第 2 行,即if之前缺少分号。
第一行并没有问题。没有分号也是完全有效的,在分号之外还可以有几种表达式(比如点、数学运算符、赋值、指针等)。
因此,在上一行报告错误可能并不总是有意义的。看这个例子:
SomeFunction(x)
+= 10
- 5
// blank line
// blank line
if (y) {
}

哪一行有错误?是带有-5的那一行吗?还是其中的一行注释?对于编译器来说,错误实际上出现在“if”语句中,因为它是第一个可以检测到有问题的地方。要报告不同的行,编译器必须报告最后一个已正确解析的标记作为错误,而不是发现错误的第一个位置。这听起来有点反常规,而且说//blank line1缺少分号更加令人困惑,因为将其更改为//blank line;当然不会改变或修复错误。
顺便说一下,这不仅适用于C或C++,这是大多数解析器报告错误的常见方式。

也许更有趣的问题是为什么有些编译器比其他编译器更详细地指出错误发生在哪一行。我猜这可能与预处理器有很大关系。有些编译器只在需要时从源代码中提取标记,而其他编译器可能会更积极地提取标记并记录它们来自哪一行。你同意这个理论吗? - supercat
如果你指的是C预处理器,那么我不同意。我听说过的几乎所有C预处理器都是以相同的方式工作的,而大多数C++错误(特别是丑陋的模板错误)与C预处理几乎没有关系。 - SoapBox

4

简单来说,这是因为解析过程的原因。当解析器期望出现;时,却遇到了if,那么错误就在于if。最简单且合理的报错方式就是说在if之前应该有一个;


3
由于以下代码是正确的:

因此,以下代码将是正确的:

SomeFunction(x)

;if(bSomeCondition)
{
}

那是因为多余的空格会被忽略。

3
编译器与空格无关。它不知道(或不在乎)语句之间是否有回车、制表符或空格。它只关心分号后面或前面的内容,或者括号('{','}')后面/前面的内容,这些括号结束和开始类和函数。这就是原因 :)

3

因为当解析该行时,它还不知道您想在那里使用分号。让我们看一个例子:

int mystuff

这行代码是否缺少分号?这取决于接下来的内容。例如,以下结构是完全正确的:

int mystuff
   = 1;

我不会像那样写,但对于编译器来说是可以的。


2
简要回答:你可以在第266行中加入分号;,这样编译器就不会报错了。从编译器的角度来看,错误就在那里。
你可以尝试使用clang,虽然我不知道它是否会为这种特定类型的错误提供更好的错误消息,但总的来说,它给出了很多更清晰的错误消息

1
因为编译器检查整个语句。让我举个例子:
int a,b,c
c=a+b;
cout<<c;

这段代码产生编译错误,提示“在 c/line 2 前需要 ;”,原因是编译器首先查看第一行的 int a,b,c,而编译器不知道是否还有其他变量或语句,因此编译器移动到第二行(因为空格是允许的),然后发现有一个语句“c=a+b”,编译器知道出现了问题,因为它期望的是一个变量或分号 (;)。因此,编译器告诉我们,在语句前需要加上分号。

简而言之,编译器不会在语句后寻找分号(如果是这样的话,我们可能无法在代码中使用空格),它会在下一个语句之前寻找分号,因为编译器不知道第一个语句有多长。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接