c ++不一致的无符号到有符号的减法结果仅对于一种排列失败

c++ inconsistent unsigned to signed subtraction results only fails for one permutation

我意识到有一个规则,宽度小于 int 的数字可以提升为更宽的类型以进行加法运算。但是我无法完全解释以下 print_unsafe_minus 中只有一个排列会失败的原因。怎么只有 <unsigned, long> 示例失败了,程序员在最佳实践方面的收获是什么?

#include <fmt/core.h>

template<typename M, typename N>
void print_unsafe_minus() {
        M a = 3, b = 4;
        N c =  a - b;
        fmt::print("{}\n", c);
}
int main() {
    // storing result of unsigned 3 minus 4 to a signed type

    print_unsafe_minus<uint8_t, int8_t>(); // -1
    print_unsafe_minus<uint16_t, int8_t>(); // -1
    print_unsafe_minus<uint32_t, int8_t>(); // -1
    print_unsafe_minus<uint64_t, int8_t>(); // -1

    print_unsafe_minus<uint8_t, int16_t>(); // -1
    print_unsafe_minus<uint16_t, int16_t>(); // -1
    print_unsafe_minus<uint32_t, int16_t>(); // -1
    print_unsafe_minus<uint64_t, int16_t>(); // -1

    print_unsafe_minus<uint8_t, int32_t>(); // -1
    print_unsafe_minus<uint16_t, int32_t>(); // -1
    print_unsafe_minus<uint32_t, int32_t>(); // -1
    print_unsafe_minus<uint64_t, int32_t>(); // -1

    print_unsafe_minus<uint8_t, int64_t>(); // -1
    print_unsafe_minus<uint16_t, int64_t>(); // -1
    print_unsafe_minus<uint32_t, int64_t>(); // 4294967295
    print_unsafe_minus<uint64_t, int64_t>(); // -1
}

(edit) 另外值得注意——如果我们扩展示例以包含 128 位整数,那么以下两个排列也会失败:

print_unsafe_minus<uint32_t, __int128>(); // 4294967295
print_unsafe_minus<uint64_t, __int128>(); // 18446744073709551615

让我们假设一个正常的二补码平台,其中 int 有 32 位并且 uint32_tunsigned 相同。

    uint32_t a = 3, b = 4;
    int64_t c =  a - b;

- 运算符的操作数经过 integral promotions*. int cannot represent all values of uint32_t, but 32-bit unsigned can represent all values of uint32_t. The values are promoted to unsigned. The result type of - is the common type of operands after promotions - both operands are unsigned. The result type of - operator is unsigned. a - b is mathematically -1. The result is (unsigned)-1, but unsigned cannot represent negative numbers. So -1 is converted to 一个 unsigned 类型,它“环绕”并导致 UINT_MAX,等于 UINT32_MAX ,因为 unsigned 有 32 位。此结果可在 int64_t 中表示,因此不会发生转换,并且 c 被分配了 UINT32_MAX.

的值

相比之下,让我们举个例子<uint16_t, int64_t>。一个32位的int可以表示一个uint16_t的所有值,所以uint16_t提升为int,所以[=24=的结果] 只是一个 (int)-1。没有从 (int)-1 到无符号数的转换。那么int64_t可以表示-1,所以值-1只是赋值给一个类型为int64_t.

的变量

* 在C语言中叫做整数提升...

在我们开始之前,让我们假设 OP 正在使用 32 位 int 类型的实现。即 int32_t 等同于 int.

设X为M的宽度,Y为N的宽度。

让我们将您的测试用例分为三类:

第一类:X <= 16

Integer promotions applies here, which is always done before invoking an arithmetic operator.

uint8_tuint16_t 的整个值范围可以用 int 表示,因此在进行减法之前它们被提升为 int。然后你从 3 - 4 得到一个带符号的值 -1,然后用它来初始化一个带符号的整数类型,无论它的宽度如何,它都可以容纳 -1。因此你得到 -1 作为输出。

第二类:(X >= 32) 和 (X >= Y)

在做减法之前没有提升。

此处适用的规则是无符号整数运算 is always modulo 2X,其中 X 是整数的宽度。

因此 a - b 总是给你 2X - 1,因为这是 [=22= 范围内等于 -1 模 2 的值].

现在你将它分配给一个有符号的类型。让我们假设 C++20(在 C++20 之前,分配无法由目标有符号类型表示的无符号值时,这是实现定义的行为)。

此处 a - b 的结果(即 2X - 1)是 converted to the unique value that is congruent to itself 模 2Y在目标范围内(即从 -2Y-1 到 2Y-1 - 1)。由于 X >= Y,这总是 -1.

所以你得到 -1 作为输出。

第三类:(X >= 32) 和 (X < Y)

这一类只有一种情况,即M = uint32_t, N = uint64_t.

的情况

减法与类别 2 相同,得到 232 - 1.

转换成有符号类型的规则还是一样的。然而,这一次,232 - 1 等于自身模 264,所以值保持不变。

注:4294967295 == 232 - 1

带走

这可能是 C++ 的一个令人惊讶的方面,正如@NathanOliver 所建议的,您应该避免混合使用有符号类型和无符号类型,并且在您确实想要混合它们时要格外小心。

您可以通过打开 -Wconversion 告诉编译器为此类转换生成警告。您的代码 gets a lot of warnings 开启时。