LLVM 项目博客

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

Clang 17 中的诊断改进

引言

在过去几个月里,我一直在参与一项持续改进 Clang 诊断功能的项目。新发布的 Clang 17 将这些改进中的几个带到了最前沿。这篇博文旨在全面概述这些诊断增强功能。我们将使用简化的代码示例,并比较 Clang 16 和 Clang 17 的诊断输出,以说明最新的更新如何增强 Clang 用户的开发体验。

代码片段的多行打印

Clang 17 最受期待的诊断功能之一是对代码片段多行打印的支持。这标志着对旧的单行限制的突破,该限制曾经使人们难以完全理解代码问题周围的上下文。这项新功能通过显示代码的更完整视图,提高了诊断消息的可读性和可理解性。此外,行号现在附加在每行左侧,便于快速导航和问题解决。

int func(
  int a, int b, int& r);

void test(int *ptr) {
  func(3, 4, 5);
  func(3, 4);
}

之前

<source>:5:3: error: no matching function for call to 'func'
  func(3, 4, 5);
  ^~~~
<source>:1:5: note: candidate function not viable: expects an lvalue for 3rd argument
int func(
    ^
<source>:6:3: error: no matching function for call to 'func'
  func(3, 4);
  ^~~~
<source>:1:5: note: candidate function not viable: requires 3 arguments, but 2 were provided
int func(
    ^

之后

<source>:5:3: error: no matching function for call to 'func'
    5 |   func(3, 4, 5);
      |   ^~~~
<source>:1:5: note: candidate function not viable: expects an lvalue for 3rd argument
    1 | int func(
      |     ^
    2 |   int a, int b, int& r);
      |                 ~~~~~~
<source>:6:3: error: no matching function for call to 'func'
    6 |   func(3, 4);
      |   ^~~~
<source>:1:5: note: candidate function not viable: requires 3 arguments, but 2 were provided
    1 | int func(
      |     ^
    2 |   int a, int b, int& r);
      |   ~~~~~~~~~~~~~~~~~~~~

在这个例子中,新覆盖的源范围使理解为什么重载候选无效变得更容易。

提交:https://reviews.llvm.org/D147875 (Timm Bäder)

  • Clang 会在宏重新定义时发出警告。当重新定义发生在汇编文件中,并且宏的先前定义来自命令行时,最后一个定义现在被诊断为来自<command line>而不是<built-in>

汇编文件

#define MACRO 3

Clang 调用命令

clang -DMACRO=1 file.S

之前

warning: 'MACRO' macro redefined [-Wmacro-redefined]
#define MACRO 3
        ^
<built-in>:362:9: note: previous definition is here
#define MACRO 1
        ^

之后

warning: 'MACRO' macro redefined [-Wmacro-redefined]
    1 | #define MACRO 3
      |         ^
<command line>:1:9: note: previous definition is here
    1 | #define MACRO 1
      |         ^

提交:https://reviews.llvm.org/D145397 (John Brawn)


  • Clang 17 在任何语言定义的内置宏未定义或重新定义时发出警告,其中一些在 Clang 16 中只是被忽略了。
#undef __cplusplus

之前:没有警告

之后

<source>:1:8: warning: undefining builtin macro [-Wbuiltin-macro-redefined]
    1 | #undef __cplusplus
      |        ^

重新定义编译器内置宏通常会导致意外结果,因为库头文件经常依赖于这些宏,并且它们不希望这些宏被用户修改。

提交:https://reviews.llvm.org/D144654 (John Brawn)


  • Clang 17 会在#pragma clang|GCC diagnostic push|pop指令后诊断意外标记。
#pragma clang diagnostic push ignore

之前:没有警告

之后

<source>:1:31: warning: unexpected token in pragma diagnostic [-Wunknown-pragmas]
    1 | #pragma clang diagnostic push ignored
      |                               ^

提交:https://github.com/llvm/llvm-project/commit/7ff507f1448bfdfcaa91d177d1f655dcb17557e7 (Aaron Ballman)

  • Clang 17 为指向未修饰函数名的ifunc/alias属性生成注释和修复提示。
__attribute__((used)) static void *resolve_foo() { return 0; }

__attribute__((ifunc("resolve_foo"))) void foo();

之前

<source>:3:16: error: ifunc must point to a defined function
__attribute__((ifunc("resolve_foo"))) void foo();
               ^

之后

<source>:3:16: error: ifunc must point to a defined function
    3 | __attribute__((ifunc("resolve_foo"))) void foo();
      |                ^
<source>:3:16: note: the function specified in an ifunc must refer to its mangled name
<source>:3:16: note: function by that name is mangled as "_ZL11resolve_foov"
    3 | __attribute__((ifunc("resolve_foo"))) void foo();
      |                ^~~~~~~~~~~~~~~~~~~~
      |                ifunc("_ZL11resolve_foov")

使用ifuncalias属性时,需要了解 C++ 名称修饰,但对于许多人来说,从函数签名中了解修饰的名称并非易事。此更改使错误消息更易于理解,因为它建议ifunc需要引用修饰的名称,并且它还通过表示修饰的名称使此错误更易于操作。

提交:https://reviews.llvm.org/D143803 (Dhruv Chawla)


  • Clang 17 通过优先考虑-Wunreachable-code-fallthrough来避免在以前从-Wunreachable-code-Wunreachable-code-fallthrough发出的不可到达[[fallthrough]];语句上出现重复警告。
void f(int n) {
  switch (n) {
    [[fallthrough]];
  case 1:;
  }
}

Clang 调用命令

clang++ -Wunreachable file.cpp

之前

<source>:3:5: warning: code will never be executed [-Wunreachable-code]
    [[fallthrough]];
    ^~~~~~~~~~~~~~~~
<source>:3:5: warning: fallthrough annotation in unreachable code [-Wunreachable-code-fallthrough]

之后

<source>:3:5: warning: fallthrough annotation in unreachable code [-Wunreachable-code-fallthrough]
    3 |     [[fallthrough]];
      |     ^

提交:https://reviews.llvm.org/D145842 (清水拓哉)


  • Clang 17 正确地为在 Clang 16 中被忽略的unavailable属性发出诊断。
template <class _ValueType = int>
class __attribute__((unavailable)) polymorphic_allocator {};

void f() { polymorphic_allocator<void> a; }

之前:无诊断

之后

<source>:4:12: error: 'polymorphic_allocator<void>' is unavailable
    4 | void f() { polymorphic_allocator<void> a; }
      |            ^
<source>:2:36: note: 'polymorphic_allocator<void>' has been explicitly marked unavailable here
    2 | class __attribute__((unavailable)) polymorphic_allocator {};
      |                                    ^

提交:https://reviews.llvm.org/D147495 (Shafik Yaghmour)


  • Clang 不再为使用__attribute__((cleanup(...)))声明的变量发出-Wunused-variable警告,以匹配 GCC 的行为。
void c(int *);
void f(void) { int __attribute__((cleanup(c))) X1 = 4; }

之前

<source>:2:48: warning: unused variable 'X1' [-Wunused-variable]
void f(void) { int __attribute__((cleanup(c))) X1 = 4; }
                                               ^

之后:没有警告

cleanup属性用于在 C 中编写 RAII。使用此属性声明的对象实际上在声明后被用作cleanup属性中指定函数的参数,因此,最好不要将其诊断为未使用。

提交:https://reviews.llvm.org/D152180 (Nathan Chancellor)

alignas 指定符

  • Clang 16 将alignas(type-id)建模为alignas(alignof(type-id))。Clang 17 修复了这种建模,从而修复了关于alignas_Alignas的诊断中错误提及alignof的问题。
struct alignas(void) A {};

之前

<source>:1:16: error: invalid application of 'alignof' to an incomplete type 'void'
struct alignas(void) A {};
              ~^~~~~

之后

<source>:1:16: error: invalid application of 'alignas' to an incomplete type 'void'
    1 | struct alignas(void) A {};
      |               ~^~~~~

提交:https://reviews.llvm.org/D150528 (yronglin)

隐藏

  • Clang 17 在 lambda 的捕获变量隐藏模板参数时发出错误。
auto h = [y = 0]<typename y>(y) { return 0; }

之前:无错误

之后

<source>:1:11: error: declaration of 'y' shadows template parameter
    1 | auto h = [y = 0]<typename y>(y) { return 0; };
      |           ^
<source>:1:27: note: template parameter is declared here
    1 | auto h = [y = 0]<typename y>(y) { return 0; };
      |                           ^

提交:https://reviews.llvm.org/D148712 (Mariya Podchishchaeva)


  • Clang 17 的-Wshadow通过静态局部变量诊断隐藏。
int var;
void f() { static int var = 42; }

之前:没有警告

之后

<source>:2:23: warning: declaration shadows a variable in the global namespace [-Wshadow]
    2 | void f() { static int var = 42; }
      |                       ^
<source>:1:5: note: previous declaration is here
    1 | int var;
      |     ^

提交:https://reviews.llvm.org/D151214 (清水拓哉)

-Wformat

  • Clang 17 会诊断在格式字符串中不正确使用范围枚举类型的情况,这是一种未定义的行为。现在它还会发出一个修复提示,建议使用static_cast将其转换为其基础类型以避免未定义行为。
#include <limits.h>
#include <stdio.h>

enum class Foo : long {
  Bar = LONG_MAX,
};

int main() { printf("%ld", Foo::Bar); }

之前:没有警告

之后

<source>:8:28: warning: format specifies type 'long' but the argument has type 'Foo' [-Wformat]
    8 | int main() { printf("%ld", Foo::Bar); }
      |                      ~~~   ^~~~~~~~
      |                            static_cast<long>( )

提交:https://github.com/llvm/llvm-project/commit/3632e2f5179a420ea8ab84e6ca33747ff6130fa2 (Aaron Ballman)

提交:https://reviews.llvm.org/D153622 (Alex Brachet)


  • Clang 17 的-Wformat%lb%lB识别为格式说明符。
#include <cstdio>
int main() { printf("%lb %lB", 10L, 10L); }

之前

<source>:2:23: warning: length modifier 'l' results in undefined behavior or no effect with 'b' conversion specifier [-Wformat]
int main() { printf("%lb %lB", 10L, 10L); }
                     ~^~
<source>:2:27: warning: length modifier 'l' results in undefined behavior or no effect with 'B' conversion specifier [-Wformat]
int main() { printf("%lb %lB", 10L, 10L); }
                         ~^~

之后:没有警告

%b%B是 ISO C23 草案中指定用于打印整数二进制表示的新格式。已经有几个支持此格式的 libc 实现可用。(例如 glibc >= 2.35)

Clang 16 已经将%b%llb识别为有效的格式说明符,但将%lb视为无效。Clang 17 识别%lb%lB以避免误报警告并发出正确的修复提示。

提交:https://reviews.llvm.org/D148779 (宋方锐)

  • Clang 通常在静态断言失败时打印==||&&等二元运算符的子表达式值,以帮助用户了解失败的原因。Clang 17 在二元运算符为||时停止打印子表达式值,因为在这种情况下很明显两个子表达式都计算为false

  • 静态断言失败的错误消息现在指向断言的表达式,而不是static_assert标记。

constexpr bool a = false;
constexpr bool b = false;
static_assert(a || b);

之前

<source>:3:1: error: static assertion failed due to requirement 'a || b'
static_assert(a || b);
^             ~~~~~~
<source>:3:17: note: expression evaluates to 'false || false'
static_assert(a || b);
              ~~^~~~

之后

<source>:3:15: error: static assertion failed due to requirement 'a || b'
    3 | static_assert(a || b);
      |               ^~~~~~

提交:https://reviews.llvm.org/D147745 (Jorge Pinto Sousa)

提交:https://reviews.llvm.org/D146376 (Krishna Narayanan)


  • Clang 17 将对 constexpr 评估中空函数指针的调用诊断为“空函数指针”,而不是仅仅说它无效。
constexpr int call(int (*F)()) {
    return F();
}
static_assert(call(nullptr));

之前

<source>:4:15: error: static assertion expression is not an integral constant expression
static_assert(call(nullptr));
              ^~~~~~~~~~~~~
<source>:2:12: note: subexpression not valid in a constant expression
    return F();
           ^
<source>:4:15: note: in call to 'call(nullptr)'
static_assert(call(nullptr));
              ^

之后

<source>:4:15: error: static assertion expression is not an integral constant expression
    4 | static_assert(call(nullptr));
      |               ^~~~~~~~~~~~~
<source>:2:12: note: 'F' evaluates to a null function pointer
    2 |     return F();
      |            ^
<source>:4:15: note: in call to 'call(nullptr)'
    4 | static_assert(call(nullptr));
      |               ^~~~~~~~~~~~~

提交:https://reviews.llvm.org/D145793 (清水拓哉)


  • 成员函数调用更接近于用户编写的代码。
struct Foo {
  constexpr int div(int i) const { return 1 / i; }
};

constexpr Foo obj;
constexpr const Foo &ref = obj;
static_assert(ref.div(0));

之前

<source>:7:15: error: static assertion expression is not an integral constant expression
static_assert(ref.div(0));
              ^~~~~~~~~~
<source>:2:45: note: division by zero
  constexpr int div(int i) const { return 1 / i; }
                                            ^
<source>:7:19: note: in call to '&obj->div(0)'
static_assert(ref.div(0));
                  ^

之后

<source>:7:15: error: static assertion expression is not an integral constant expression
    7 | static_assert(ref.div(0));
      |               ^~~~~~~~~~
<source>:2:45: note: division by zero
    2 |   constexpr int div(int i) const { return 1 / i; }
      |                                             ^ ~
<source>:7:15: note: in call to 'ref.div(0)'
    7 | static_assert(ref.div(0));
      |               ^~~~~~~~~~

提交:https://reviews.llvm.org/D151720 (清水拓哉)


  • 当 constexpr 变量的构造函数调用使它的子对象未初始化时,Clang 17 会打印未初始化子对象的名称,而不是它的类型。
struct Foo {
  constexpr Foo() {}
  int val;
};
constexpr Foo ff;

之前

<source>:5:15: error: constexpr variable 'ff' must be initialized by a constant expression
constexpr Foo ff;
              ^~
<source>:5:15: note: subobject of type 'int' is not initialized
<source>:3:7: note: subobject declared here
  int val;
      ^

之后

<source>:5:15: error: constexpr variable 'ff' must be initialized by a constant expression
    5 | constexpr Foo ff;
      |               ^~
<source>:5:15: note: subobject 'val' is not initialized
<source>:3:7: note: subobject declared here
    3 |   int val;
      |       ^

提交:https://reviews.llvm.org/D146358 (清水拓哉)


  • Clang 17 将未使用的 const 变量模板诊断为“未使用的变量模板”,而不是“未使用的变量”。
namespace {
template <typename T> constexpr double var_t = 0;
}

之前

<source>:2:40: warning: unused variable 'var_t' [-Wunused-const-variable]
template <typename T> constexpr double var_t = 0;
                                       ^

之后

<source>:2:40: warning: unused variable template 'var_t' [-Wunused-template]
    2 | template <typename T> constexpr double var_t = 0;
      |                                        ^~~~~

未实例化的模板不会生成符号,因此,未使用的含义比通常的未使用变量或函数更广泛。

因此,-Wunused省略了-Wunused-template。此更改遵循了这个理由,并导致更少的无意-Wunused-const-variable警告。

提交:https://reviews.llvm.org/D152796 (清水拓哉)

鸣谢

特别感谢我的 Google 暑期科研计划导师 Timm Bäder,感谢他在这整个项目中提供的宝贵指导和支持。

还要感谢我的定期评审员:Aaron Ballman、Christopher Di Bella 和 Shafik Yaghmour,感谢他们提供的富有洞察力和建设性的反馈,这些反馈极大地改善了我的代码。