LLVM 项目博客

LLVM 项目新闻和来自前线的细节

Clang 错误恢复的惊人壮举

除了解析和为您的源文件生成机器代码(当代码有效时),编译器前端的职责还包括检测无效代码并提供提示解释问题所在,以便您能修复问题。错误可能是完全无效的(错误)或仅仅是合法但看起来非常可疑(警告)。这些错误和警告被称为编译器“诊断信息”,Clang 旨在超越职责范围,提供真正出色的体验。

在继续阅读本文后,我们将展示一些 Clang 特别努力的示例。有关其他示例,Clang 网页也有一篇关于诊断信息的页面,Doug 在一篇之前的博客文章中展示了 Clang 如何诊断两阶段名称查找问题。

更新:其他人开始比较他们喜欢的编译器。这是 OpenVMS 编译器。如果您有要发布的比较,请发送电子邮件给 Chris。

Секреты восстановления (俄语翻译)  提供者: Softdroid Recovery.
乌克兰语翻译 提供者:Sandi Wolfe。
爱沙尼亚语翻译 提供者:Johanne Teerink。
德语翻译 提供者: Philip Egger。
西班牙语翻译 提供者: Laura Mancini





这些示例使用 Apple GCC 4.2 作为这些示例的比较,但这并非意在贬低(旧版本的)GCC。许多编译器都有这些问题,我们强烈建议您在您最喜欢的编译器上尝试这些示例,看看它的表现如何。所有展示的示例都必然是展示问题的简短(简化)示例,当您在现实生活中看到这些示例时,它们通常更有说服力:)。

未知类型名


解析 C 和 C++ 时,一个令人讨厌的事情是您必须知道什么是类型名才能解析代码。例如,“(x)(y)”可以是将表达式“(y)”转换为类型“x”的强制转换,也可以是使用“(y)”参数列表调用“x”函数,这取决于 x 是类型还是函数。不幸的是,一个常见的错误是忘记包含头文件,这意味着编译器真的不知道某样东西是类型还是函数,因此必须根据上下文进行强烈的猜测。以下是一些示例

$ cat t.m
NSString *P = @"foo";
$ clang t.m
t.m:4:1: error: unknown type name 'NSString'
NSString *P = @"foo";
^
$ gcc t.m
t.m:4: error: expected '=', ',', ';', 'asm' or '__attribute__' before '*' token

以及

$ cat t.c
int foo(int x, pid_t y) {
return x+y;
}
$ clang t.c
t.c:1:16: error: unknown type name 'pid_t'
int foo(int x, pid_t y) {
^
$ gcc t.c
t.c:1: error: expected declaration specifiers or '...' before 'pid_t'
t.c: In function 'foo':
t.c:2: error: 'y' undeclared (first use in this function)
t.c:2: error: (Each undeclared identifier is reported only once
t.c:2: error: for each function it appears in.)

如果您忘记使用“struct stat”而不是“stat”,也会在 C 中出现这种情况。这篇文章的共同主题是,通过推断程序员的意图来很好地恢复,可以帮助 Clang 避免像 GCC 在第二行中发出的三行一样发出虚假的后继错误。

拼写检查器


Clang 包含的一个更明显的功能是拼写检查器。当您使用 Clang 不认识的标识符时,拼写检查器就会启动:它会检查其他相似的标识符并建议您可能要使用的内容。以下是一些示例

$ cat t.c
#include <inttypes.h>
int64 x;
$ clang t.c
t.c:2:1: error: unknown type name 'int64'; did you mean 'int64_t'?
int64 x;
^~~~~
int64_t
$ gcc t.c
t.c:2: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'x'

另一个示例是

$ cat t.c
#include <sys/stat.h>
int foo(int x, struct stat *P) {
return P->st_blocksize*2;
}
$ clang t.c
t.c:4:13: error: no member named 'st_blocksize' in 'struct stat'; did you mean 'st_blksize'?
return P->st_blocksize*2;
^~~~~~~~~~~~
st_blksize
$ gcc t.c
t.c: In function ‘foo’:
t.c:4: error: 'struct stat' has no member named 'st_blocksize'

拼写检查器很棒的一点是它可以捕获各种常见的错误,并且还能帮助以后的恢复。例如,以后使用“x”的代码知道它被声明为 int64_t,因此不会导致其他没有意义的奇怪的后继错误。Clang 使用著名的 Levenshtein 距离函数来计算所有可能的候选者中最匹配的候选者。

类型定义跟踪


Clang 会仔细跟踪您在代码中编写的类型定义,以便它能够将错误与您在代码中使用的类型相关联。这使它能够以您使用的术语打印错误消息,而不是以完全解析和模板实例化的编译器术语打印错误消息。它还使用范围信息和插入符号来显示您写的内容,而不是试图将它打印回给您。Clang 诊断信息页面上有一些关于此的示例,但再来一个示例也无妨

$ cat t.cc
namespace foo {
struct x { int y; };
}
namespace bar {
typedef int y;
}
void test() {
foo::x a;
bar::y b;
a + b;
}
$ clang t.cc
t.cc:10:5: error: invalid operands to binary expression ('foo::x' and 'bar::y' (aka 'int'))
a + b;
~ ^ ~
$ gcc t.cc
t.cc: In function 'void test()':
t.cc:10: error: no match for 'operator+' in 'a + b'

这表明 clang 会以您键入的名称(分别为“foo::x”和“bar::y”)提供源名称,但它也会使用“aka”展开 y 类型,以防底层表示很重要。其他编译器通常会提供完全无用的信息,这些信息无法真正告诉您问题所在。这是一个来自 GCC 的出乎意料的简洁示例,但它似乎也缺少一些关键信息(例如,为什么没有匹配项)。此外,如果表达式不仅仅是单个“a+b”,您可以想象将它重新打印回给您并不是最有帮助的。

最令人讨厌的解析


许多初学者程序员犯的一个错误是,他们无意中定义了函数而不是堆栈上的对象。这是由于 C++ 语法中的歧义导致的,这种歧义是以任意方式解决的。这是 C++ 不可避免的一部分,但至少编译器应该帮助您理解问题所在。以下是一个简单的示例

$ cat t.cc
#include <vector>

int foo() {
std::vector<std::vector<int> > X();
return X.size();
}
$ clang t.cc
t.cc:5:11: error: base of member reference has function type 'std::vector<std::vector<int> > ()'; perhaps you meant to call this function with '()'?
return X.size();
^
()
$ gcc t.cc
t.cc: In function ‘int foo()’:
t.cc:5: error: request for member ‘size’ in ‘X’, which is of non-class type ‘std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > > ()()’

当我最初声明向量为接受某些参数(例如“10”来指定初始大小)但重构代码并消除这些参数时,我遇到了这个问题。当然,如果您不删除括号,代码实际上是在声明一个函数,而不是一个变量。

在这里,您可以看到 Clang 清楚地指出我们已经声明了一个函数(它甚至提供了在您忘记 () 的情况下帮助您调用它的方法)。另一方面,GCC 既对您正在做什么感到困惑,又吐出您没有写过的很大的类型名(std::allocator 从哪里来的?)。令人遗憾但却是事实,成为经验丰富的 C++ 程序员实际上意味着您擅长破译编译器吐给您的错误消息。

如果您继续尝试更经典的示例,在那里这种问题会困扰人们,您会看到 Clang 更加努力地尝试

$ cat t.cc
#include <fstream>
#include <vector>
#include <iterator>

int main() {
std::ifstream ifs("file.txt");
std::vector<char> v(std::istream_iterator<char>(ifs),
std::istream_iterator<char>());

std::vector<char>::const_iterator it = v.begin();
return 0;
}
$ clang t.cc
t.cc:8:23: warning: parentheses were disambiguated as a function declarator
std::vector<char> v(std::istream_iterator<char>(ifs),
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
t.cc:11:45: error: member reference base type 'std::vector<char> (*)(std::istream_iterator<char>, std::istream_iterator<char> (*)())' is not a structure or union
std::vector<char>::const_iterator it = v.begin();
~ ^
$ gcc t.cc
t.cc: In function ‘int main()’:
t.cc:11: error: request for member ‘begin’ in ‘v’, which is of non-class type
‘std::vector<char, std::allocator<char> > ()(std::istream_iterator<char, char, std::char_traits<char>, long int>, std::istream_iterator<char, char, std::char_traits<char>, long int> (*)())’

在这种情况下,Clang 的第二个错误并不特别好(尽管它确实提供了更简洁的类型名),但它提供了一个非常重要的警告,告诉您示例中的括号是在声明一个函数,而不是用作参数的括号。

缺少分号


我经常犯的一个错误(可能是由于 C++ 语法过于不一致,也可能是因为我粗心大意并且注意力不集中……)就是丢掉分号。幸运的是,一旦您知道问题所在,这些问题就很容易修复,但它们会导致某些编译器发出一些非常令人困惑的错误消息。即使在对人类来说问题立即显而易见的情况下(如果他们注意到了!),也会发生这种情况。例如

$ cat t.c
struct foo { int x; }

typedef int bar;
$ clang t.c
t.c:1:22: error: expected ';' after struct
struct foo { int x; }
^
;
$ gcc t.c
t.c:3: error: two or more data types in declaration specifiers

请注意,GCC 在问题之后的东西上发出了错误。如果结构体是头文件结尾处的最后一项,这意味着您最终将在与问题所在文件完全不同的文件中收到错误消息。这个问题在 C++ 中也会加剧(就像许多其他问题一样),例如

$ cat t2.cc
template<class t>
class a{}

class temp{};
a<temp> b;

class b {
}
$ clang t2.cc
t2.cc:2:10: error: expected ';' after class
class a{}
^
;
t2.cc:8:2: error: expected ';' after class
}
^
;
$ gcc t2.c
t2.cc:4: error: multiple types in one declaration
t2.cc:5: error: non-template type ‘a’ used as a template
t2.cc:5: error: invalid type in declaration before ‘;’ token
t2.cc:8: error: expected unqualified-id at end of input

除了发出令人困惑的错误“在一个声明中有多种类型”之外,GCC 还会以其他方式让自己感到困惑。

. 与 -> 的错误


在 C++ 代码中,指针和引用通常可以互换使用,并且经常使用 . 来代替 ->。Clang 识别出这种常见的错误,并帮助您解决

$ cat t.cc
#include <map>

int bar(std::map<int, float> *X) {
return X.empty();
}
$ clang t.cc
t.cc:4:11: error: member reference type 'std::map<int, float> *' is a pointer; maybe you meant to use '->'?
return X.empty();
~^
->
$ gcc t.cc
t.cc: In function ‘int bar(std::map<int, float, std::less<int>, std::allocator<std::pair<const int, float> > >*)’:
t.cc:4: error: request for member ‘empty’ in ‘X’, which is of non-class type ‘std::map<int, float, std::less<int>, std::allocator<std::pair<const int, float> > >*’

除了帮助您了解指针是“非类类型”之外,它还会不遗余力地拼出 std::map 的完整定义,这当然没有帮助。

:: 与 : 的错字


也许只有我才会这样,但我倾向于在匆忙中犯这个错误。C++ :: 运算符用于分隔嵌套名称说明符,但不知何故我一直输入 :。以下是一个展示这个想法的最小示例

$ cat t.cc
namespace x {
struct a { };
}

x:a a2;
x::a a3 = a2;
$ clang t.cc
t.cc:5:2: error: unexpected ':' in nested name specifier
x:a a2;
^
::
$ gcc t.cc
t.cc:5: error: function definition does not declare parameters
t.cc:6: error: ‘a2’ was not declared in this scope

除了正确地获取错误消息(并建议将“::”替换为“::”)之外,Clang “知道您的意思”,因此它会正确地处理 a2 的后续使用。相反,GCC 会对错误感到困惑,导致它对 a2 的每次使用都发出虚假的错误。这可以通过稍微详细的示例来展示

$ cat t2.cc
namespace x {
struct a { };
}

template <typename t>
class foo {
};

foo<x::a> a1;
foo<x:a> a2;

x::a a3 = a2;
$ clang t2.cc
t2.cc:10:6: error: unexpected ':' in nested name specifier
foo<x:a> a2;
^
::
t2.cc:12:6: error: no viable conversion from 'foo<x::a>' to 'x::a'
x::a a3 = a2;
^ ~~
t2.cc:2:10: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'foo<x::a>' to 'x::a const' for 1st argument
struct a { };
^
$ gcc t2.cc
t2.cc:10: error: template argument 1 is invalid
t2.cc:10: error: invalid type in declaration before ‘;’ token
t2.cc:12: error: conversion from ‘int’ to non-scalar type ‘x::a’ requested

在这里您可以看到,Clang 的第二个错误消息是完全正确的(并且得到了解释)。GCC 给出了一个令人困惑的后继消息,内容是关于将“int”转换为 x::a。从哪里来的“int”?

在近乎绝望的情况下提供帮助


C++ 是一种强大的工具,它给了您足够的绳子去把自己吊死,同时也让您混淆您的多范式隐喻。不幸的是,这种力量给了您很多机会让自己陷入近乎绝望的境地,您知道“有错误”,但不知道真正的错误是什么,也不知道如何修复它。值得庆幸的是,Clang 试图帮助您,即使是在最困难的时候。例如,以下是一个涉及歧义查找的示例

$ cat t.cc
struct B1 { void f(); };
struct B2 { void f(double); };

struct I1 : B1 { };
struct I2 : B1 { };

struct D: I1, I2, B2 {
using B1::f; using B2::f;
void g() {
f();
}
};
$ clang t.cc
t.cc:10:5: error: ambiguous conversion from derived class 'D' to base class 'B1':
struct D -> struct I1 -> struct B1
struct D -> struct I2 -> struct B1
f();
^
$ gcc t.cc
t.cc: In member function ‘void D::g()’:
t.cc:10: error: ‘B1’ is an ambiguous base of ‘D’

在这种情况下,您可以看到 clang 不仅告诉您存在歧义,它还准确地告诉您导致问题的继承层次结构中的确切路径。当您处理非平凡的层次结构时,并且所有类都不在同一个文件里,您可以看到它,这可以真正帮助您解决问题。

公平地说,GCC 偶尔也会尝试提供帮助。不幸的是,当它这样做时,不清楚它带来的帮助比带来的危害更多。例如,如果您在上面的示例中注释掉两个 using 声明,您会得到

$ clang t.cc
t.cc:10:5: error: non-static member 'f' found in multiple base-class subobjects of type 'B1':
struct D -> struct I1 -> struct B1
struct D -> struct I2 -> struct B1
f();
^
t.cc:1:18: note: member found by ambiguous name lookup
struct B1 { void f(); };
^
$ gcc t.cc
t.cc: In member function ‘void D::g()’:
t.cc:10: error: reference to ‘f’ is ambiguous
t.cc:2: error: candidates are: void B2::f(double)
t.cc:1: error: void B1::f()
t.cc:1: error: void B1::f()
t.cc:10: error: reference to ‘f’ is ambiguous
t.cc:2: error: candidates are: void B2::f(double)
t.cc:1: error: void B1::f()
t.cc:1: error: void B1::f()

看起来 GCC 正在尝试,但为什么它在第 10 行上发出两个错误,以及为什么它在每个错误中都打印了两次 B1::f?当我遇到这些错误时(这非常罕见,因为我不经常使用像这样的多重继承),我真的重视清晰度来解开正在发生的事情。

还有一件事……合并冲突


好吧,这可能有点过分,但您还能如何完全爱上一个编译器呢?

$ cat t.c
void f0() {
<<<<<<< HEAD
int x;
=======
int y;
>>>>>>> whatever
}
$ clang t.c
t.c:2:1: error: version control conflict marker in file
<<<<<<< HEAD
^
$ gcc t.c
t.c: In function ‘f0’:
t.c:2: error: expected expression before ‘<<’ token
t.c:4: error: expected expression before ‘==’ token
t.c:6: error: expected expression before ‘>>’ token

是的,clang 实际上检测到了合并冲突,并解析了冲突的一侧。您不希望您的编译器对如此简单的错误发出大量无意义的信息,对吧?

Clang:专为可能偶尔会犯错误的真正程序员设计。为什么要满足于更差的东西呢?

-Chris