LLVM 项目博客

LLVM 项目新闻和来自战壕的详细信息

Clang 警告


Clang 有两种类型的诊断:错误和警告。错误是代码不符合语言规范时出现的。例如,缺少分号、大括号不匹配等会导致编译失败,Clang 会发出错误消息。

另一方面,警告是在符合语言规范的代码中出现可疑结构时发出的。随着时间的推移,人们发现某些代码模式有很高的可能是编程错误。例如,运算顺序混淆、将相似名称的语言特性混淆以及易于出现的、虽然仍然是有效代码但会导致错误的拼写错误。

虽然警告可能存在误报,但尽早发现错误的实用性通常超过其缺点。继续阅读以了解 Clang 警告的演示,以及与 GCC 警告的比较。

以下代码包含不到 200 行代码、一个库和一个仅用于打印的头文件。它是合法的 C++ 代码,可以编译成程序。花几分钟时间看看你能否在以下代码中找到任何错误。



main.cc

  1. #include "sort.h"
  2. #include <iostream>
  3. int main(int argc, char** argv) {
  4.  int V[] = { 3, 4, 7, 10, 11, 1, 2, 0};
  5.  cout << "Unsorted numbers:" << endl;
  6.  for( auto num : V )
  7.    cout << " " << num << endl;
  8.  if (!sort(V, sizeof(V)/sizeof(V[0]))) {
  9.    cout << "Sort failed." << endl;
  10.    return 1;
  11.  }
  12.  cout << "Sorted numbers:" << endl;
  13.  for( auto num : V )
  14.    cout << " " << num << endl;
  15.  return 0;
  16. }


sort.h

  1. #ifndef _EXPERIMENTAL_WARNINGS_SORT_H_
  2. #define _EXPERIMNETAL_WARNINGS_SORT_H_
  3. #include <iostream>
  4. #ifdef _NDEBUG
  5. #define ASSERT(cond) \
  6.    if(!cond) cout << __FILE__  << ":" <<  __LINE__ << " " << #cond << endl;
  7. #else
  8. #define ASSERT(cond) if (!cond) {}
  9. #endif
  10. enum SortType {
  11.  unknown = 0,
  12.  min_invalid = 3,
  13.  bubble = 1,
  14.  quick,
  15.  insert
  16. };
  17. class Sort {
  18. public
  19.  Sort(int vec[], int size, bool sorted = false);
  20.  bool IsSorted();
  21.  void Begin(SortType Type = unknown);
  22. private
  23.  void BubbleSort();
  24.  void QuickSort() { }; // 尚未实现。
  25.  void InsertSort() { }; // 尚未实现。
  26.  int* vec_;
  27.  bool sorted_;
  28.  int &size_;
  29. };
  30. static bool sort(int vec[], int size) {
  31.  Sort sort(vec, size);
  32.  sort.Begin(bubble);
  33.  return sort.IsSorted();
  34. }
  35. #endif // _EXPERIMENTAL_WARNINGS_SORT_H_


sort.cc
  1. #include <iostream>
  2. #include "sort.h"
  3. Sort::Sort(int vec[], int size, bool sorted)
  4.    : sorted_(sorted_), vec_(vec), size_(size) {
  5.  if (size > 50)
  6.    ASSERT("!Vector too large.  Number of elements:" + size);
  7.  int sum;
  8.  for (unsigned i = 0; !i == size; ++i) {
  9.    int sum = sum + vec_[i];
  10.    ++i;
  11.  }
  12.  ASSERT(sum < 100 && "Vector sum is too high");
  13. }
  14. bool Sort::IsSorted() {
  15.  return sort;
  16. }
  17. static bool CheckSort(int V[]) {
  18.  bool ret;
  19.  for (int i = 1; i != sizeof(V)/sizeof(V[0]); ++i)
  20.    if (V[i] > V[i - 1])
  21.      ret = false;
  22.  return ret;
  23. }
  24. static const char* TypeToString(SortType Type) {
  25.  const char* ret;
  26.  switch (Type) {
  27.    case bubble
  28.      ret = "bubble";
  29.    case quick
  30.      ret = "quick";
  31.    case insert
  32.      ret = "insert";
  33.  }
  34.  return ret;
  35. }
  36. void Sort::Begin(SortType Type) {
  37.  cout << "Sort type: ";
  38.  cout << Type == 0 ? "Unknown type, resorting to bubble sort"
  39.                    : TypeToString(Type);
  40.  cout << endl;
  41.  switch (Type) {
  42.    default
  43.    bubble
  44.      BubbleSort(); break;
  45.    quick
  46.      QuickSort(); break;
  47.    insert
  48.      InsertSort(); break;
  49.  }
  50.  sorted_ = CheckSort(vec_);
  51. }
  52. void Sort::BubbleSort() {
  53.  for (int i = 0; i < size_; ++i) {
  54.    for (int j = 1; j < size_; ++i) {
  55.      int a = vec_[j-1];
  56.      int b = vec_[j];
  57.      if (a > b); {
  58.        vec_[j-1] = b;
  59.        vec_[j] = a;
  60.      }
  61.    }
  62.  }
  63. }

你找到任何错误了吗?许多常见问题很难仅仅通过阅读代码来发现。为了获得更好的编码体验,Clang 提供了许多诊断功能,可以标记这些错误。代码中的错误详述如下。

main.cc 没有问题。它仅仅是库的包装器,以便可以生成和运行二进制文件,尽管运行它不会正确排序数组。

sort.h 是库的头文件。

1:#ifndef _EXPERIMENTAL_WARNINGS_SORT_H_
2:#define _EXPERIMNETAL_WARNINGS_SORT_H_

第一个警告是在前两行触发的。头文件保护符用于防止库文件被多次包含导致重定义错误。为了正常工作,`#ifndef` 和 `#define` 必须使用相同的宏名称。E 和 N 的位置颠倒会导致不同的名称,这是一种很容易被忽视的错误。更糟糕的是,这种错误可能会隐藏在头文件中,在单个包含时不会产生问题,而在稍后的某个时间点,当有人多次包含此头文件时才会开始产生问题。Clang 的 `-Wheader-guard` 选项可以捕获此错误。GCC 不会捕获此错误。

接下来,检查使用的自定义 ASSERT 宏

7:#define ASSERT(cond) \
8.    if(!cond) cout << ...

问题在于将宏参数视为函数参数。宏参数不会被求值。相反,它们会被直接替换成相应的类型。因此,代码 `ASSERT(x == 5)` 会变成 `if(!x == 5) cout << ...`。正确的解决方法是在宏参数周围加上括号,例如 `if (!(cond)) cout << ...`。`-Wlogical-not-parentheses` 可以捕获此错误。由于位于宏定义中,因此当使用宏时,该警告会触发,并包含指向此处的注释。GCC 没有与 `-Wlogical-not-parentheses` 等效的选项。

13:enum SortType {
14:  unknown = 0,
15:  min_invalid = 3,
16:
17:  bubble = 1,
18:  quick,
19:  insert
20:};

在这个枚举中,定义了几个无效的值,然后列出有效枚举。有效枚举使用自动递增来获取其值。但是,`min_invalid` 和 `insert` 的值都是 3。幸运的是,`-Wduplicate-enum` 会识别出这种情况下的枚举并将其标记出来。GCC 不会对此发出警告。

继续 sort.cc
类构造函数
4:Sort::Sort(int vec[], int size, bool sorted)
5:    : sorted_(sorted_), vec_(vec), size_(size) {

来自 sort.h 的成员
34:  int* vec_;
35:  bool sorted_;
36:  int &size_;

检查类的唯一构造函数,可以在这里看到许多问题。首先,请注意变量声明为 `vec_`、`sorted_`,然后是 `size_`,但在构造函数中,它们被列为 `sorted_`、`vec_`,然后是 `size_`。初始化顺序是声明顺序,这意味着 `vec_` 在 `sorted_` 之前初始化。这里没有顺序依赖关系,但 `-Wreorder` 会警告说顺序不匹配。GCC 也有 `-Wreorder`。

接下来,`sorted_` 使用自身而不是 sorted 初始化。这会导致 `sorted_` 中的值未初始化,这会被恰如其分的 `-Wuninitialized` 捕获。对于这种情况,GCC 有 `-Wself-assign` 和 `-Wself-init`。

最后,请注意 `size_` 被声明为引用,但 size 不是通过引用传递的。size 只在构造函数结束前存在,而引用 `size_` 将继续指向它。`-Wdangling-field` 会捕获此问题。

7:    ASSERT("!Vector too large.  Number of elements:" + size);

这里有两个问题。将整数添加到字符串字面量不会将两者连接起来。相反,由于字符串字面量是指向类型为 `const char *` 的指针,因此实际上执行了指针运算。对于足够大的整数,这甚至会导致指针越过字符串的末尾进入其他内存。`-Wstring-plus-int` 会警告此情况。GCC 没有等效的警告。

7:    ASSERT("!Vector too large.  More than 50 elements.");

修复后,另一个问题出现了。一个常见的模式是包含一个描述断言的字符串字面量。如果断言总是要触发,那么表达式应该求值为 false。字符串字面量求值为 true,所以只需将其取反就可以得到一个 false 值,对吧?好吧,可能会出现一些常见的拼写错误。

这些值求值为 true
"true"
"false"
"!true"
"!false"
"any string"

这些值求值为 false
!"true"
!"false"
!"any string"
!"!any string"

因此,`-Wstring-conversion` 会警告将字符串字面量转换为 true 布尔值的情况。请使用 `ASSERT(false && “string”)` 或 `ASSERT(0 && “string”)` 替代。GCC 没有等效的警告。

9:  int sum;
10:  for (unsigned i = 0; !i == size; ++i) {
11:    int sum = sum + vec_[i];
12:    ++i;
13:  }
14:
15:  ASSERT(sum < 100 && "Vector sum is too high");

这里还有更多错误。 最明显的错误是 sum 在第 9 行没有被初始化。 当它在第 15 行被使用时,会导致未定义的行为。 这可以通过 -Wuninitialized 警告捕获,该警告建议将它设置为 0 来修复。GCC 的 -Wuninitialized 也会捕获这个错误。

一个错误的取反导致了 for 循环中的错误条件。 !i == size 等同于 (!i) == size 由于运算符优先级的缘故。 -Wlogical-not-parentheses 会建议使用 !(i == size) 来修正这个问题。同样,GCC 没有 -Wlogical-not-parantheses 的等效项。

在第 10 行和第 12 行,对于每次循环迭代,两个 ++i 会增加变量的值。 -Wloop-analysis 中的一个警告会捕获这个错误。这个警告是 Clang 特定的。

第 11 行出现了两个独立的问题。 这里还有一个 -Wuninitialized 警告,因为它在初始化自身时使用了 sum。 同时,-Wshadow 会警告说循环内部的变量 sum 与循环外部的变量 sum 不同。GCC 和 Clang 版本的这两个警告都会捕获这些问题。

18:bool Sort::IsSorted() {
19:  return sort;
20:}

成员变量叫做 sorted_。 sort 是一个在 sort.h 文件的第 39 行定义的静态函数包装器。 在这里,函数会自动转换为一个函数指针,然后转换为 true。 被 -Wbool-conversion 警告捕获。GCC 会在 -Waddress 中捕获函数地址到 bool 的转换。

22:static bool CheckSort(int V[]) {
23:  bool ret;
24:  for (int i = 1; i != sizeof(V)/sizeof(V[0]); ++i)
25:    if (V[i] > V[i - 1])
26:      ret = false;
27:  return ret;
28:}

在第 23 行,ret 没有被初始化。 在第 26 行,可能给它赋值,但有可能没有代码路径会给它赋值。 警告 -Wsometimes-uninitialized 会为此发出警告,并给出修复建议。GCC 不会捕获这个错误。

在第 24 行的 for 循环条件中,有 i != sizeof(V)/sizeof(V[0]),它试图使 i 保持为一个有效的数组索引。 然而,sizeof 计算的结果是错误的。 如果 V 在函数内部声明,那么这段代码是正确的。 然而,作为参数,V 的类型是 int*,导致 sizeof 获取的是指针的大小,而不是数组的大小。 需要给函数传递一个单独的大小参数。被 -Wsizeof-array-argument 警告捕获,这是一个 Clang 特定的警告。

30:static const char* TypeToString(SortType Type) {
31:  const char* ret;
32:  switch (Type) {
33:    case bubble
34:      ret = "bubble";
35:    case quick
36:      ret = "quick";
37:    case insert
38:      ret = "insert";
39:  }
40:  return ret;
41:}

一个简单的枚举到字符串转换函数。 与上一个函数一样,ret 仍然没有被初始化,并且会被 -Wsometimes-uninitialized 警告捕获。 Clang 会在此发出警告,因为它可以分析 switch 中的所有路径,并确定至少有一条路径不会给 ret 赋值。GCC 没有 -Wsometimes-uninitialized。

在 switch 语句中,Clang 和 GCC 会注意到 unknown 没有被使用,被警告 -Wswitch 捕获。

Clang 还有一个特殊的属性,[[clang::fallthrough]];,用于标记 switch case 中的故意贯穿。 通过标记所有 case 中故意使用的贯穿,Clang 可以对意外的情况发出警告,例如这三个没有 break 语句的 case。 否则,贯穿会导致所有三个 case 都返回 “insert”。这是一个 -Wimplicit-fallthrough 警告,这是一个 Clang 特定的警告。

43:void Sort::Begin(SortType Type) {
44:  cout << "Sort type: ";
45:  cout << Type == 0 ? "Unknown type, resorting to bubble sort"
46:                    : TypeToString(Type);
47:  cout << endl;

虽然 cout 使用 operator<< 将各种类型流到输出,但它仍然是一个运算符,并且 C++ 对运算符优先级有定义。 移位运算符(<< 和 >>)比条件运算符(?:)的优先级高。 为了添加一些括号来明确优先级,它可以变为:

45:  ((cout << Type) == 0) ? "Unknown type, resorting to bubble sort"
46:                        : TypeToString(Type);

首先,Type 通过 operator<< 被推送到 cout 中。 operator<< 再次返回 cout 流。 流可以转换为 bool 来检查它们是否有效,并与 false 进行比较,false 从 0 转换而来。 这个比较的结果用于条件运算符,其结果在任何地方都没有被使用。GCC 不会捕获这个错误,但 Clang 的 -Woverloaded-shift-op-parentheses 会捕获这个错误。

48:  switch (Type) {
49:    default
50:    bubble
51:      BubbleSort(); break;
52:    quick
53:      QuickSort(); break;
54:    insert
55:      InsertSort(); break;
56:  }

另一个 switch 语句,也会触发 -Wswitch,因为没有一个枚举值被表示。 每个枚举值都缺少一个 case,导致它们成为标签而不是 case。 警告 -Wunused-label 提示这里有错误。-Wunused-label 存在于 GCC 和 Clang 中。

61:  for (int i = 0; i < size_; ++i) {
62:    for (int j = 1; j < size_; ++i) {
        ...
69:    }
70:  }

这个双重嵌套循环给冒泡排序带来了 n2 的时间复杂度。 实际上,在这种情况下,它的时间复杂度是无限的。 请注意,两个循环的递增都发生在 i 上,即使是在内层循环中。 j 在这里或循环内部都没有被访问过。 -Wloop-analysis 会在 for 循环条件中所有变量在循环迭代期间都没有发生变化时发出警告。仅在 Clang 中。

63:      int a = vec_[j-1];
64:      int b = vec_[j];
65:      if (a > b); {
66:        vec_[j-1] = b;
67:        vec_[j] = a;
68:      }

然后,-Wempty-body 警告会在第 65 行触发。 这里的分号变成了整个循环体,而值交换发生在每次循环迭代中,而不仅仅是在条件为真时。Clang 和 GCC 都有这个警告。

这只是 Clang 提供的警告的一小部分示例。除了提供信息丰富的诊断消息外,这些警告还能帮助程序员避免常见的编码陷阱。特别是,它可以节省程序员在以后跟踪有效但并非预期的代码时的时间。这些警告使 Clang 成为程序员提高生产力和代码质量的出色工具。

为了参考,上面讨论的警告列表如下:

-Wbool-conversion
警告隐式将函数指针转换为 true bool 值。GCC 将这个警告作为 -Waddress 的一部分。

-Wempty-body
当 if 语句、while 循环、for 循环或 switch 语句的体中只有分号,并且与语句的其余部分在同一行时,会发出警告。Clang 和 GCC 警告。

-Wheader-guard
#ifndef 和 #define 中的宏名称不同。这会导致头文件保护无法防止多次包含。 这是一个 Clang 特定的警告,在 SVN 主干中可用,计划在 3.4 版本中发布。

-Wimplicit-fallthrough
启用此警告将导致对所有未注释的 switch 语句中的穿透发出诊断。需要在 C++11 模式下编译。Clang 特定的警告,在 3.2 版本中可用。

-Wlogical-not-parentheses
此警告是 -Wparentheses 的一部分。这组警告建议在用户可能以不同于编译器的形式阅读代码的情况下使用括号。此特定警告将在逻辑非运算符 ('!') 应用于比较左侧时触发,而它原本应该应用于整个条件。"!x == y" 与 "!(x == y)" 不同,因为 "!x" 将在相等比较之前进行评估。 这是一个 Clang 特定的警告,在 SVN 主干中可用,计划在 3.4 版本中发布。

-Wloop-analysis
检测循环中的可疑代码。它捕获了两种有趣的循环模式:for 循环在其标头中有一个递增/递减,并且在循环体中的最后一个语句中具有相同的递增/递减,这将仅执行预期迭代次数的一半,以及当 for 循环比较中的变量在循环期间未被修改时,可能指示无限循环。Clang 特定的警告。未修改循环变量警告在 3.2 及更高版本中可用。双重递增/递减的检测在 SVN 主干中,计划在 3.4 版本中发布。

-Woverloaded-shift-op-parentheses
重载的移位运算符主要用于流,例如 cin 和 cout。但是,移位运算符的优先级高于某些运算符,例如比较运算符,这可能会导致意外问题。此警告建议使用括号来消除对求值顺序的歧义。自 3.3 版本起,Clang 特定的警告可用。


-Wshadow
内部作用域中的变量名称可能与外部作用域中的变量名称相同,但引用不同的变量。然后,在内部作用域内,可能难以或无法引用外部作用域中的变量。-Wshadow 指出了这种变量名称重复使用的情况。Clang 和 GCC 都有此警告。

-Wsizeof-array-argument
当尝试获取作为参数的数组的 sizeof() 时发出警告。这些数组被视为指针,将返回意外的大小。Clang 特定的警告。

-Wstring-conversion
文字字符串类型为 const char *。作为指针,它可以转换为 bool,但在大多数情况下,转换为 bool 不是预期的。此警告将在这种转换时触发。Clang 特定的警告。

-Wswitch
此警告检测到当在 switch 中使用枚举类型时,枚举的某些值不在 case 中表示。Clang 和 GCC 警告。

-Wuninitialized
当声明变量时,其值为未初始化。如果在初始化之前在其他地方使用其值,则可能会导致未定义的行为。Clang 和 GCC 中都有。Clang 执行额外的分析,例如跟踪多条代码路径。Clang 警告捕获的一些项目在 GCC 中属于不同的警告,例如 -Wself-assign 和 -Wself-init。

-Wunique-enum
枚举元素可以显式声明为一个值,也可以隐式声明,这将使其值为前一个元素的值加一。自 3.3 版本起,Clang 特定的警告可用。

-Wunused-label
当代码中存在标签但从未被调用时发出警告。Clang 和 GCC 警告。

* 2013 年 9 月 10 日编辑。已进行拼写更正。3.2 或更高版本中首次可用的警告已注明。如果没有标记,则该警告自 3.2 版本之前就一直存在。