表达式诊断

除了快速和功能强大之外,我们还致力于使 Clang 极其用户友好。就命令行编译器而言,这基本上归结为使编译器生成的诊断(错误和警告消息)尽可能有用。我们有几种方法可以做到这一点。本节讨论命令行编译器提供的体验,在某些情况下将 Clang 输出与 GCC 4.9 的输出进行对比。

列号和插入符号诊断

首先,clang 生成的所有诊断都包含完整的列号信息。clang 命令行编译器驱动程序使用此信息来打印“点诊断”。(IDE 可以使用此信息在行内显示错误标记。)这很好,因为它使您很容易理解特定代码段中到底出了什么问题。

该点(绿色“^”字符)准确地显示了问题所在,即使在字符串内部也是如此。这使得跳转到问题变得非常容易,并且在同一行出现多个相同字符时很有帮助。(我们将在接下来的示例中更详细地讨论这一点。)

  $ clang -fsyntax-only format-strings.c
  format-strings.c:91:13: warning: '.*' specified field precision is missing a matching 'int' argument
    printf("%.*d");
              ^

请注意,现代版本的 GCC 已经效仿了 Clang 的做法,现在能够为诊断提供一个列号,并在结果中包含一段源代码。但是,Clang 的列号更加准确,指向有问题的格式说明符,而不是解析器在检测到问题时到达的字符。)此外,Clang 的诊断默认情况下是彩色的,这使得它更容易与附近的文本区分开来。

相关文本的范围突出显示

Clang 捕获并准确地跟踪程序中表达式、语句和其他结构的范围信息,并使用它来使诊断突出显示相关信息。在以下有点荒谬的示例中,您甚至不需要查看原始源代码就可以根据 Clang 错误理解问题所在。因为 clang 打印了一个点,所以您确切地知道它抱怨的是哪个加号。范围信息突出显示了加号的左侧和右侧,这使您立即明白编译器在说什么。范围信息对于涉及优先级问题和许多其他情况的情况非常有用。

  $ gcc-4.9 -fsyntax-only t.c
  t.c: In function 'int f(int, int)':
  t.c:7:39: error: invalid operands to binary + (have 'int' and 'struct A')
     return y + func(y ? ((SomeA.X + 40) + SomeA) / 42 + SomeA.X : SomeA.X);
                                         ^
  $ clang -fsyntax-only t.c
  t.c:7:39: error: invalid operands to binary expression ('int' and 'struct A')
    return y + func(y ? ((SomeA.X + 40) + SomeA) / 42 + SomeA.X : SomeA.X);
                         ~~~~~~~~~~~~~~ ^ ~~~~~

措辞的精确性

一个细节是,我们非常努力地使 clang 输出的诊断包含有关问题是什么以及原因的准确信息。在上面的示例中,我们告诉您左侧和右侧的推断类型是什么,并且我们没有重复从该点显而易见的信息(例如,这是一个“二元+”)。

还有很多其他例子。在以下示例中,我们不仅告诉您存在问题*并指向它,我们还准确地说出原因,并告诉您类型是什么(如果它是一个复杂的子表达式,例如对重载函数的调用)。这种对细节的关注使得更容易理解和快速解决问题。

  $ gcc-4.9 -fsyntax-only t.c
  t.c:5:11: error: invalid type argument of unary '*' (have 'int')
    return *SomeA.X;
           ^
  $ clang -fsyntax-only t.c
  t.c:5:11: error: indirection requires pointer operand ('int' invalid)
    int y = *SomeA.X;
            ^~~~~~~~

类型定义保留和选择性解包

许多程序员使用高级用户定义类型、类型定义和其他语法糖来引用程序中的类型。这很有用,因为它们可以缩写否则非常长的类型,并且保留类型名称在诊断中很有用。但是,有时非常简单的类型定义可以包装平凡类型,并且剥离类型定义以了解正在发生的事情非常重要。Clang 旨在很好地处理这两种情况。

以下示例显示了在 C 中保留类型定义的重要性。

  $ clang -fsyntax-only t.c
  t.c:15:11: error: can't convert between vector values of different size ('__m128' and 'int const *')
    myvec[1]/P;
    ~~~~~~~~^~

以下示例显示了编译器公开类型定义的底层细节的用处。如果用户对系统“pid_t”类型定义的定义感到困惑,Clang 会用“aka”帮助显示它。

  $ clang -fsyntax-only t.c
  t.c:13:9: error: member reference base type 'pid_t' (aka 'int') is not a structure or union
    myvar = myvar.x;
            ~~~~~ ^

在 C++ 中,类型保留包括保留写入类型名称的任何限定符。例如,如果我们取一小段代码,例如

namespace services {
  struct WebService {  };
}
namespace myapp {
  namespace servers {
    struct Server {  };
  }
}

using namespace myapp;
void addHTTPService(servers::Server const &server, ::services::WebService const *http) {
  server += http;
}

然后编译它,我们会看到 Clang 既提供了准确的信息,又保留了用户编写的类型(例如,“servers::Server”、“::services::WebService”)。

  $ clang -fsyntax-only t.cpp
  t.cpp:9:10: error: invalid operands to binary expression ('servers::Server const' and '::services::WebService const *')
    server += http;
    ~~~~~~ ^  ~~~~

自然地,类型保留扩展到模板的使用,Clang 保留有关特定模板特化(如std::vector<Real>)在源代码中拼写方式的信息。例如

  $ clang -fsyntax-only t.cpp
  t.cpp:12:7: error: incompatible type assigning 'vector<Real>', expected 'std::string' (aka 'class std::basic_string<char>')
    str = vec;
        ^ ~~~

修复提示

“修复”提示提供有关修复源代码中小型局部问题的建议。当 Clang 生成有关它可以解决的特定问题的诊断(例如,非标准或多余的语法、缺少关键字、常见错误等)时,它还可能以代码转换的形式提供具体指南来更正问题。在以下示例中,Clang 警告使用自 1993 年以来被认为已过时的 GCC 扩展。应删除下划线的代码,然后用点行(“.x =”或“.y =”,分别)下面的代码替换。

  $ clang t.c
  t.c:5:28: warning: use of GNU old-style field designator extension
  struct point origin = { x: 0.0, y: 0.0 };
                          ~~ ^
                          .x = 
  t.c:5:36: warning: use of GNU old-style field designator extension
  struct point origin = { x: 0.0, y: 0.0 };
                                  ~~ ^
                                  .y = 

“修复”提示对于解决常见的用户错误和误解最为有用。例如,C++ 用户通常会忘记类模板显式特化的语法,如以下示例中的错误所示。同样,在描述问题之后,Clang 将修复 - 添加template<> - 作为诊断的一部分。

  $ clang t.cpp
  t.cpp:9:3: error: template specialization requires 'template<>'
    struct iterator_traits<file_iterator> {
    ^
    template<> 

模板类型差异

模板类型可能很长且难以阅读。更不用说作为错误消息的一部分了。Clang 不会仅仅打印出类型名称,而是有足够的信息来删除公共元素并突出显示差异。为了更清晰地显示模板结构,模板类型也可以打印为缩进的文本树。

默认:使用类型省略的模板差异
t.cc:4:5: note: candidate function not viable: no known conversion from 'vector<map<[...], float>>' to 'vector<map<[...], double>>' for 1st argument;
-fno-elide-type:没有省略的模板差异
t.cc:4:5: note: candidate function not viable: no known conversion from 'vector<map<int, float>>' to 'vector<map<int, double>>' for 1st argument;
-fdiagnostics-show-template-tree:带有省略的模板树打印
t.cc:4:5: note: candidate function not viable: no known conversion for 1st argument;
  vector<
    map<
      [...],
      [float != double]>>
-fdiagnostics-show-template-tree -fno-elide-type:没有省略的模板树打印
t.cc:4:5: note: candidate function not viable: no known conversion for 1st argument;
  vector<
    map<
      int,
      [float != double]>>

自动宏展开

许多错误发生在有时嵌套很深的宏中。使用传统编译器,您需要深入研究宏的定义才能理解您是如何遇到麻烦的。以下简单的示例显示了 Clang 如何通过自动打印实例化信息和嵌套范围信息来帮助您诊断它们是如何通过宏实例化的,还显示了其他一些部分如何在更大的示例中工作。

  $ clang -fsyntax-only t.c
  t.c:80:3: error: invalid operands to binary expression ('typeof(P)' (aka 'struct mystruct') and 'typeof(F)' (aka 'float'))
    X = MYMAX(P, F);
        ^~~~~~~~~~~
  t.c:76:94: note: expanded from:
  #define MYMAX(A,B)    __extension__ ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; })
                                                                                           ~~~ ^ ~~~

这是在“window”Unix 包(它实现“wwopen”类 API)中发生的另一个真实世界警告

  $ clang -fsyntax-only t.c
  t.c:22:2: warning: type specifier missing, defaults to 'int'
          ILPAD();
          ^
  t.c:17:17: note: expanded from:
  #define ILPAD() PAD((NROW - tt.tt_row) * 10)    /* 1 ms per char */
                  ^
  t.c:14:2: note: expanded from:
          register i; \
          ^

在实践中,我们发现 Clang 对宏的处理实际上在多重嵌套宏中比在简单宏中更有用。

实现质量和对细节的关注

最后,我们投入了大量精力来完善细节,因为细枝末节随着时间的推移会累积起来,并为用户带来良好的体验。

以下示例显示了我们比 GCC 更好地从忘记在结构定义后添加分号的简单情况下恢复过来。

  $ cat t.cc
  template<class T>
  class a {};
  struct b {}
  a<int> c;
  $ gcc-4.9 t.cc
  t.cc:4:8: error: invalid declarator before 'c'
   a<int> c;
           ^
  $ clang t.cc
  t.cc:3:12: error: expected ';' after struct
  struct b {}
             ^
             ;

以下示例显示了我们诊断并从缺少typename关键字恢复过来,即使在 GCC 无法应对的复杂情况下也是如此。

  $ cat t.cc
  template<class T> void f(T::type) { }
  struct A { };
  void g()
  {
      A a;
      f<A>(a);
  }
  $ gcc-4.9 t.cc
  t.cc:1:33: error: variable or field 'f' declared void
   template<class T> void f(T::type) { }
                                   ^
  t.cc: In function 'void g()':
  t.cc:6:5: error: 'f' was not declared in this scope
       f<A>(a);
       ^
  t.cc:6:8: error: expected primary-expression before '>' token
       f<A>(a);
          ^
  $ clang t.cc
  t.cc:1:26: error: missing 'typename' prior to dependent type name 'T::type'
  template<class T> void f(T::type) { }
                           ^~~~~~~
                           typename 
  t.cc:6:5: error: no matching function for call to 'f'
      f<A>(a);
      ^~~~
  t.cc:1:24: note: candidate template ignored: substitution failure [with T = A]: no type named 'type' in 'A'
  template<class T> void f(T::type) { }
                         ^    ~~~~

虽然这些细节中的每一个都很小,但我们认为它们加起来提供了更加完善的体验。