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")
使用ifunc
或alias
属性时,需要了解 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 (宋方锐)
与 constexpr 相关的诊断
-
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,感谢他们提供的富有洞察力和建设性的反馈,这些反馈极大地改善了我的代码。