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_t
与 unsigned
相同。
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_t
和 uint16_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 开启时。
我意识到有一个规则,宽度小于 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_t
与 unsigned
相同。
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_t
和 uint16_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 开启时。