【问题标题】:c++ inconsistent unsigned to signed subtraction results only fails for one permutationc ++不一致的无符号到有符号减法结果仅对一个排列失败
【发布时间】:2021-06-09 23:51:40
【问题描述】:

我意识到有一条规则可以将宽度小于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
}

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

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

【问题讨论】:

  • 你知道促销规则是怎么说的吗?所有这些例子都应该可以通过精确地遵循它们来解释。当你试图解释这些情况下的规则时,你能解释一下你的解释以及它与发生的情况到底有什么不同吗?
  • @NateEldredge 如果uint8_tuint16_t 被提升为与uint32_t 相同的宽度,那么它们是否也不会像int64_t 那样失败?
  • 程序员在最佳实践方面的收获是什么? 不要混用有符号和无符号。真的,unsigned 类型只需要按位运算。
  • only the &lt;unsigned, long&gt; 仅来自您的代码 uint32_t, int64_t 失败,而不是 unsigned, long
  • @NathanOliver 好吧,有一些 STL 容器类型,还有它们烦人的 size_t 东西!

标签: c++ unsigned underflow


【解决方案1】:

在开始之前,让我们假设 OP 正在使用 32 位 int 类型的实现。也就是说,int32_t 等价于int

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

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

第一类:X

Integer promotions 在这里适用,总是在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,因为这是在 M 的范围内等于 -1 模 2 的值。

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

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

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

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

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

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

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

注意:4294967295 == 232 - 1

带走

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

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

【讨论】:

  • 所以,假设我想将这个c = a - b; 留在我的代码中,因为我知道它目前是安全的,但在未来某个时候,另一个开发人员可能会更改有符号 c 或无符号 a 或 b 的大小。什么 static_assert 将在这里封装它保持“安全”的原则,即它不会从负向正溢出?
  • @PatrickParker 好吧,我认为这值得一个单独的问题和更多的研究。如果我错了,请纠正我,您基本上想要以下内容:给定两个整数类型,您希望在编译时获得一个可以保存操作数类型之间任何差异值的有符号类型。
  • 嗯,更像是这样:开发人员 A 控制一个标头,该标头定义了可能会更改的有符号和无符号操作数的 typedef。开发人员 B 控制代码signed_c = unsigned_a - unsigned_b,该代码不会更改。第三个开发人员(我)想使用这些变量和/或 typedef 编写一个 static_assert,以确保结果不会下溢成为正数。你是对的,它可能应该是一个单独的问题。但这正是我试图用“什么是外卖”问题来解决的问题。
  • 你能确认一下这个 static_assert 是否正确地防止了下溢到积极的情况吗? static_assert(sizeof(a-b)&gt;=sizeof(c) || sizeof(a-b)&lt;sizeof(int)); 在这里写 int 而不是 32 是否正确?
  • @PatrickParker 您正在获取两个无符号值之间的差异并将其转换为有符号值,结果可能不是您想要的(我在之前的评论中对此进行了说明)。例如,UINT_MAX- 0u0u - 1u 都会给你UINT_MAX。在进行减法之前,您必须将 ab 转换为比两者都宽的有符号类型(即 uint32_tint64_t
【解决方案2】:

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

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

- 运算符的操作数经过integral promotions*。 int不能代表uint32_t的所有值,但是32位的unsigned可以代表uint32_t的所有值。这些值被提升为unsigned-is the common type of operands 提升后的结果类型 - 两个操作数都是 unsigned- 运算符的结果类型为unsigneda - b 在数学上是 -1。结果是(unsigned)-1,但unsigned 不能表示负数。所以-1is converted tounsigned 类型,它“环绕”并导致UINT_MAX,它等于UINT32_MAX,因为unsigned 有32 位。这个结果可以在int64_t 中表示,所以不会发生转换,c 被分配了 UINT32_MAX 的值。

相比之下,我们以&lt;uint16_t, int64_t&gt; 为例。一个 32 位的int 可以表示一个uint16_t 的所有值,所以uint16_t 被提升为int,所以a - b 的结果只是一个(int)-1。没有从(int)-1 到无符号数的转换。那么int64_t可以代表-1,所以-1的值只是赋给了int64_t类型的变量。

* 在C语言中称为整数提升...

【讨论】:

  • 程序员在最佳实践方面的收获是什么?例如“不要担心将无符号 a-b 分配给有符号 c,除非 c 比 int 宽且宽度不同于 (a-b)”?
  • 好吧,我不明白那部分。最佳实践是什么?结果应该是什么?如果你想要-1,只需写-1,而不是减去两个数字。绝对最好的做法是学习语言并知道它是如何工作的。如果您不喜欢隐式提升和转换以及弱类型,请转到另一种编程语言。
  • don't worry about assigning unsigned a-b to signed c unless c is wider than int and different width than (a-b) 你想要什么结果?代码按预期工作。我相信你想要明确 - N c = (M)(a - b); - 在这种情况下它会根据无符号类型溢出(我不知道这是否是预期结果)。这完全取决于您想要什么。在任何情况下,最佳实践都可能是遵循 MISRA 和其他安全标准中的一些规则,这些规则专门处理代码中隐式提升的令人困惑的行为。
  • 我在 MISRA C++ 2008 tlemp.com/download/rule/MISRA-CPP-2008-STANDARD.pdf 中看到了前规则 5-0-3。从那以后,其他最佳做法是执行N c = (N)a - (N)b - 在计算之前转换值。但请注意,签名溢出也是一种极端情况——最好单独处理。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-05-30
  • 2016-01-02
  • 2012-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多