在 C 和 C++ 中比较不同大小的无符号整数时如何收到警告?

问题描述 投票:0回答:5

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
    成为编译时常量的解决方案。
  • 理想情况下,该解决方案适用于现有的 C/C++ 项目,而无需完全重写它们。
  • 建议除编译器警告之外的其他工具也很有用,但编译器警告本身是最好的。
c++ c gcc clang compiler-warnings
5个回答
12
投票

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
文件。

最后,我会注意到我没有对此查询进行任何“调整” 真实的代码。可能会产生很大的噪音。


5
投票

这并不能直接回答问题(提供警告),但是您会考虑一种完全避免问题的替代方案吗?

    size_t n = // ...

    for (typeof(n) i = 0; i < n; i++) // ...

现在 n 是什么类型并不重要,因为

i
始终与
n
相同,因此您永远不会遇到因
i
更小或范围更小而导致
n 导致的无限循环问题
.


4
投票

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
不是算术表达式。对于通用二进制表达式,此警告似乎没有变体。


2
投票

对于 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) {
    }
}

请注意,更改比较运算符的操作数之一的类型的必要性既是优点也是缺点:它要求您修改代码,但它也允许您对每个变量应用检查。 ..


2
投票

PVS Studio 可以发出此类警告(以及更多警告),这里是他们文档中几乎相同的示例:

https://pvs-studio.com/en/docs/warnings/v104/

这是一个付费工具,但他们为开源项目提供免费许可证。

我在 LLVM 项目的免费 linter 工具 Clang-tidy 中没有找到这样的警告,但是添加对不同大小的整数的比较的检查会非常简单(后来 Scott McPeak 的回复非常出色,clang -query 完成了大部分工作 - 剩下的部分只是将此查询插入到 clang-tidy 中)。但这会是非常吵闹的检查。人们可以通过限制对循环条件的检查来限制噪音,这也可以使用 Clang-tidy 来完成,但使用 AST 匹配器需要做更多的工作。

© www.soinside.com 2019 - 2024. All rights reserved.