C 或 C++ 中错误的常见来源是类似的代码
size_t n = // ...
for (unsigned int i = 0; i < n; i++) // ...
当
unsigned int
溢出时可以无限循环。
例如,在 Linux 上,
unsigned int
是 32 位,而 size_t
是 64 位,因此如果 n = 5000000000
,我们会陷入无限循环。
如何使用 GCC 或 clang 获得有关此问题的警告?
GCC 的
-Wall -Wextra
不这样做:
#include <stdint.h>
void f(uint64_t n)
{
for (uint32_t i = 0; i < n; ++i) {
}
}
gcc-13 -std=c17 \
-Wall -Wextra -Wpedantic \
-Warray-bounds -Wconversion \
-fanalyzer \
-c -o 76840686.o 76840686.c
(无输出)
编辑:回答评论/答案中的一些问题:
n
成为编译时常量的解决方案。gcc
或中似乎没有内置警告选项
clang
满足要求。但是,我们可以使用
clang-query
相反。
下面是一个
clang-query
命令,它将报告以下内容的比较
32 位和 64 位整数,假设 int
是 32 位且
long
是 64 位。 (下面有更多相关内容。)
#!/bin/sh
PATH=$HOME/opt/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04/bin:$PATH
# In this query, the comments are ignored because clang-query (not the
# shell) recognizes and discards them.
query='m
binaryOperator( # Find a binary operator expression
anyOf( # such that any of:
hasOperatorName("<"), # is operator <, or
hasOperatorName("<="), # is operator <=, or
hasOperatorName(">"), # is operator >, or
hasOperatorName(">="), # is operator >=, or
hasOperatorName("=="), # is operator ==, or
hasOperatorName("!=") # is operator !=;
),
hasEitherOperand( # and where either operand
implicitCastExpr( # is an implicit cast
has( # from
expr( # an expression
hasType( # whose type
hasCanonicalType( # after resolving typedefs
anyOf( # is either
asString("int"), # int or
asString("unsigned int") # unsigned int,
)
)
)
)
),
hasImplicitDestinationType( # and to a type
hasCanonicalType( # that after typedefs
anyOf( # is either
asString("long"), # long or
asString("unsigned long") # unsigned long.
)
)
)
).bind("operand")
)
)
'
# Run the query on test.c.
clang-query \
-c="set bind-root false" \
-c="$query" \
test.c -- -w
# EOF
当在以下
test.c
上运行时,它会报告所有指定的情况:
// test.c
// Demonstrate reporting comparisons of different-size operands.
#include <stddef.h> // size_t
#include <stdint.h> // int32_t, etc.
void test(int32_t i32, int64_t i64, uint32_t u32, uint64_t u64)
{
i32 < i32; // Not reported: same sizes.
i32 < i64; // reported
i64 < i64;
u32 < u32;
u32 < u64; // reported
u64 < u64;
i32 < u64; // reported
u32 < i64; // reported
i32 <= i64; // reported
i64 > i32; // reported
i64 >= i32; // reported
i32 == i64; // reported
u64 != u32; // reported
i32 + i64; // Not reported: not a comparison operator.
((int64_t)i32) < i64; // Not reported: explicit cast.
// Example #1 in question.
size_t n = 0;
for (unsigned int i = 0; i < n; i++) {} // reported
}
// Example #2 in question.
void f(uint64_t n)
{
for (uint32_t i = 0; i < n; ++i) { // reported
}
}
// EOF
有关
clang-query
命令的一些详细信息:
该命令将
-w
传递给 clang-tidy
以抑制其他警告。
那只是因为我以引发警告的方式编写测试
关于未使用的值,对于普通代码来说是不必要的。
它通过了
set bind-root false
,所以唯一报告的站点是
感兴趣的操作数而不是报告整个表达式。
查询的令人不满意的方面是它明确列出了来源 和目的地类型。不幸的是,
clang-query
没有
匹配器来报告任何 32 位类型,因此必须列出它们
单独。您可能需要添加 [unsigned] long long
目的地一侧。如果运行此命令,您可能还需要删除 [unsigned] long
具有针对 Windows 等 IL32 平台的编译器选项的代码。
相关地,请注意
clang-query
在之后接受编译器选项
--
,或者在
compile_commands.json
文件。
最后,我会注意到我没有对此查询进行任何“调整” 真实的代码。可能会产生很大的噪音。
这并不能直接回答问题(提供警告),但是您会考虑一种完全避免问题的替代方案吗?
size_t n = // ...
for (typeof(n) i = 0; i < n; i++) // ...
现在 n 是什么类型并不重要,因为
i
始终与 n
相同,因此您永远不会遇到因 i
更小或范围更小而导致 n
导致的无限循环问题
.
gcc 的最新版本似乎支持
-Warith-conversion
用于此目的:
-Warith-conversion
即使将操作数转换为相同类型不能更改其值,也要警告算术运算的隐式转换。这会影响来自
、-Wconversion
和-Wfloat-conversion
的警告。-Wsign-conversion
void f (char c, int i) { c = c + i; // warns with -Wconversion c = c + 1; // only warns with -Warith-conversion }
然而它不适用于你的例子,可能是因为
i < n
不是算术表达式。对于通用二进制表达式,此警告似乎没有变体。
对于 C++,假设
n
是编译时间常量,您可能可以做得比编译器警告更好。这也适用于非 gcc 编译器。但此逻辑不适用于 C 代码。
这个想法基本上是将值信息编码在变量类型中,而不是变量值中。
template<std::integral T, auto N>
constexpr bool operator<(T value, std::integral_constant<decltype(N), N>)
{
static_assert(std::is_signed_v<T> == std::is_signed_v<decltype(N)>, "the types have different signs");
static_assert((std::numeric_limits<T>::max)() >= N, "the maximum of type T is smaller than N");
return value < N;
}
// todo: overload with swapped operator parameter types
int main()
{
constexpr std::integral_constant<size_t, 500'000'000> n; // go with 5'000'000'000, and you'll get a compiler error
for (unsigned int i = 0; i < n; i++)
{
}
}
如果该值不是编译时常量,您仍然可以为整数创建包装模板类型并重载
<
运算符以与整数值进行比较,将 static_assert
添加到该运算符的主体中。
template<std::integral T>
class IntWrapper
{
T m_value;
public:
constexpr IntWrapper(T value)
: m_value(value)
{}
template<std::integral U>
friend constexpr bool operator<(U o1, IntWrapper o2)
{
static_assert(std::is_signed_v<U> == std::is_signed_v<T>, "types have different signedness");
static_assert((std::numeric_limits<U>::max)() >= (std::numeric_limits<T>::max)(),
"the comparison may never yield false because of the maxima of the types involved");
return o1 < o2.m_value;
}
};
void f(IntWrapper<uint64_t> n)
{
for (uint32_t i = 0; i < n; ++i) {
}
}
请注意,更改比较运算符的操作数之一的类型的必要性既是优点也是缺点:它要求您修改代码,但它也允许您对每个变量应用检查。 ..
PVS Studio 可以发出此类警告(以及更多警告),这里是他们文档中几乎相同的示例:
https://pvs-studio.com/en/docs/warnings/v104/
这是一个付费工具,但他们为开源项目提供免费许可证。
我在 LLVM 项目的免费 linter 工具 Clang-tidy 中没有找到这样的警告,但是添加对不同大小的整数的比较的检查会非常简单(后来 Scott McPeak 的回复非常出色,clang -query 完成了大部分工作 - 剩下的部分只是将此查询插入到 clang-tidy 中)。但这会是非常吵闹的检查。人们可以通过限制对循环条件的检查来限制噪音,这也可以使用 Clang-tidy 来完成,但使用 AST 匹配器需要做更多的工作。