【问题标题】:Is comparing an underflowed, unsigned integer to -1 well-defined?是否将下溢的无符号整数与 -1 进行了明确的比较?
【发布时间】:2015-02-08 21:47:20
【问题描述】:

考虑以下†:

size_t r = 0;
r--;
const bool result = (r == -1);

结果初始化为result 的比较是否具有明确定义的行为?
它的结果true 是否如我所料?


写这个问答是因为我特别不确定两个因素。
在我的回答中,它们都可以通过使用“关键[ly]”一词来识别。

† 这个例子的灵感来自于计数器无符号时循环条件的方法:
for (size_t r = m.size() - 1; r != -1; r--)

【问题讨论】:

  • 另一个小问题:严格来说,r-- 的行为不是“下溢”,尽管非正式地这样称呼它是合理的。
  • 我花了一点时间才记住“下溢”的含义(当数字太小而无法在浮点类型中存储为非零时)。我想一个更好的术语可能是“负溢出”。
  • @mwfearnley:也许,但可以说更糟,因为标准需要时间(在注释中)解释无符号溢出是不可能的。当然,按照同样的逻辑,无符号下溢是不可能的,但至少它没有这么明确地说明。 ;) 是的,我什么都没有。
  • 依赖于实现。这台机器可能是值得称赞的。
  • 嗯,我总是使用:const bool result = (r == (size_t)-1);。只是为了确保编译将-1 解释为size_t 而不是r 作为其他类型变量。但是您的选择也可能是正确的,只需要看看标准。

标签: c++ c++11 language-lawyer unsigned-integer signed-integer


【解决方案1】:
size_t r = 0;
r--;
const bool result = (r == -1);

严格来说,result 的值是实现定义的。在实践中,几乎可以肯定是true;如果有一个false 的实现,我会感到惊讶。

r--之后的r的值就是SIZE_MAX的值,这个宏定义在<stddef.h>/<cstddef>中。

为了比较r == -1,通常的算术转换在两个操作数上执行。通常算术转换的第一步是将积分提升应用于两个操作数。

r 是 size_t 类型,实现定义的无符号整数类型。 -1 是 int 类型的表达式。

在大多数系统上,size_t 至少与int 一样宽。在这样的系统上,整数提升会导致 r 的值被转换为 unsigned int 或保持其现有类型(如果 size_t 与 int 具有相同的宽度,则前者可能发生,但转换较低秩)。现在左操作数(无符号)至少具有右操作数(有符号)的等级。右操作数转换为左操作数的类型。此转换产生与r 相同的值,因此相等比较产生true。

这是“正常”情况。

假设我们有一个实现,其中size_t 是 16 位(假设它是 typedef 对应于 unsigned short)而 int 是 32 位。所以SIZE_MAX == 65535 和INT_MAX == 2147483647。或者我们可以有一个 32 位的 size_t 和一个 64 位的 int。我怀疑是否存在任何此类实现,但标准中没有任何内容禁止它(见下文)。

现在比较的左侧有类型size_t 和值65535。由于有符号int 可以表示size_t 类型的所有值,因此整数提升将值转换为65535int 类型。 == 运算符的两边都有int 类型,因此通常的算术转换无关。表达式等价于65535 == -1,明明就是false。

正如我所提到的,这种事情不太可能发生在size_t 类型的表达式上——但它很容易发生在更窄的无符号类型上。例如,如果r 被声明为unsigned short,或unsigned char,甚至是在该类型被签名的系统上的普通char,则result 的值可能是false。 (我说可能是因为short 甚至unsigned char 可以与int 具有相同的宽度,在这种情况下result 将是true。)

在实践中,您可以通过显式进行转换而不是依赖实现定义的常用算术转换来避免潜在问题:

const bool result = (r == (size_t)-1);

或

const bool result = (r == SIZE_MAX);

C++11 标准参考:

  • 5.10 [expr.eq] 等式运算符
  • 5.9 [expr.rel] 关系运算符(指定执行通常的算术转换)
  • 5 [expr] 表达式,第 9 段:通常的算术转换
  • 4.5 [conv.prom] 整体促销
  • 18.2 [support.types] size_t

18.2 第 6-7 段:

6 size_t 类型是实现定义的无符号整数类型 足以容纳任何对象的大小(以字节为单位)。

7 [ 注意: 建议实现选择类型 ptrdiff_t 和 size_t 的整数转换等级 (4.13) 没有 大于signed long int,除非更大的尺寸 必须包含所有可能的值。 ——尾注]

所以没有禁止使size_t 比int 窄。我几乎可以合理地想象一个系统,其中int 是 64 位,但没有单个对象可以大于 232-1 字节,因此 size_t 是 32 位。

【讨论】:

  • 嗯,是的几乎我想。
  • 具有 32 位可寻址存储器的 64 位 ALU,还是具有 32 位页面的基于页面的存储器系统?某种特殊用途的流处理芯片?
  • @KeithThompson:我真的希望 C 标准委员会能够指定一种声明“包装”无符号类型的方法,这些类型不会隐式转换或提升为有符号类型[例如的总和或乘积。 wrap32_t 和 int 将是 wrap32_t,无论 int 是 16、32 还是 64 位]。这样的类型可以编写真正可移植的干净代码,并确保需要任何可移植性所需的类型转换。没有这样的语言功能...
  • ...我看不到任何实用的方法来迁移假设uint32_t 不会被提升为签名类型的代码[一个非常普遍的假设] 到int 的任何系统大于 32 位。
  • @supercat:和/或 C++ 标准委员会(这个问题被标记为 C++,而不是 C)。我同意,虽然我还没有想过如何定义它。有许多有用的整数类型:size_t、ptrdiff_t、uintN_t 等,理想情况下它们应该以明确定义的方式协同工作。积分促销在int 和unsigned int 方面的定义如此强烈,这与其他类型具有未指定的关系,这一事实令人讨厌。这个想法是不需要支持比int窄的类型的整数运算(这对很多硬件来说很方便),但是...
【解决方案2】:

是的,结果如您所愿。

让我们分解一下。

此时r 的值是多少?好吧,下溢是明确定义的,导致r 在运行比较时达到最大值。 std::size_t has no specific known bounds,但与int 相比,我们可以对其范围做出合理的假设:

std::size_t 是 sizeof 运算符结果的无符号整数类型。 [..]std::size_t 可以存储理论上可能的任何类型(包括数组)对象的最大大小。

而且,为了避免妨碍,表达式-1 是应用于文字1 的一元-,并且在任何系统上都具有int 类型:

[C++11: 2.14.2/2]: 整数文字的类型是表 6 中可以表示其值的相应列表中的第一个。 [..]

(我不会引用所有描述如何将一元 - 应用于 int 导致 int 的文本,但确实如此。)

建议在大多数系统上,int 将无法容纳 std::numeric_limits<std::size_t>::max(),这是非常合理的。

现在,这些操作数会发生什么?

[C++11: 5.10/1]: ==(等于)和!=(不等于)运算符与关系运算符具有相同的语义限制、转换和结果类型,但它们的优先级较低且结果为真值. [..]

[C++11: 5.9/2]: 通常的算术转换是在算术或枚举类型的操作数上执行的。 [..]

让我们来看看这些“常见的算术转换”:

[C++11: 5/9]: 许多期望算术或枚举类型的操作数的二元运算符会以类似的方式导致转换和产生结果类型。目的是产生一个通用类型,也就是结果的类型。

这种模式称为常用的算术转换,定义如下:

  • 如果任一操作数属于范围枚举类型 (7.2),则不执行任何转换;如果另一个 操作数的类型不同,表达式格式不正确。
  • 如果任一操作数的类型为 long double,则另一个应转换为 long double`。
  • 否则,如果任一操作数为double,则另一个应转换为double。
  • 否则,如果任一操作数为float,则另一个应转换为float。
  • 否则,应在两个操作数上执行整数提升 (4.5)。59 然后应将以下规则应用于提升的操作数:
    • 如果两个操作数的类型相同,则无需进一步转换。
    • 否则,如果两个操作数都具有有符号整数类型或都具有无符号整数类型,则 具有较小整数转换等级类型的操作数应转换为 具有更高等级的操作数。
    • 否则,如果无符号整数类型的操作数的秩大于或等于另一个操作数类型的秩,则将有符号整数类型的操作数转换为 无符号整数类型的操作数的类型。
    • 否则,如果带符号整数类型的操作数的类型可以表示无符号整数类型的操作数类型的所有值,则将无符号整数类型的操作数转换为带符号整数类型的操作数的类型输入。
    • 否则,两个操作数都应转换为对应的无符号整数类型 带符号整数类型的操作数的类型。

我已经强调了这里生效的段落,至于为什么:

[C++11: 4.13/1]:每个整数类型都有一个整数转换等级,定义如下

  • [..]
  • long long int的rank大于long int的rank,int的rank大于short int的rank,大于short int的rank signed char 的排名。
  • 任何无符号整数类型的等级应等于相应有符号整数类型的等级。
  • [..]

所有整数类型,即使是固定宽度的,都由标准整数类型组成;因此,从逻辑上讲,std::size_t 必须是 unsigned long long、unsigned long 或 unsigned int。

  • 如果std::size_t 是unsigned long long 或unsigned long,则std::size_t 的等级大于unsigned int 的等级,因此也大于int。

  • 如果std::size_t 是unsigned int,则std::size_t 的等级等于unsigned int 的等级,因此也等于int 的等级。

无论哪种方式,根据通常的算术转换,有符号操作数都被转换为无符号操作数的类型(而且,至关重要的是,不是反过来!) .现在,这种转换意味着什么?

[C++11: 4.7/2]: 如果目标类型是无符号的,则结果值是与源整数一致的最小无符号整数(模 2n 其中 n 是用于表示无符号类型的位数)。 [ 注意: 在二进制补码表示中,这种转换是概念性的,在位模式(如果没有截断)。 ——尾注]

[C++11: 4.7/3]:如果目标类型是有符号的,如果可以在目标类型(和位域宽度)中表示,则值不变;否则,该值是实现定义的。

这意味着std::size_t(-1)等价于std::numeric_limits<std::size_t>::max();至关重要的是,上述子句中的值 n 与用于表示 unsigned 类型的位数相关,而不是源类型。否则,我们将使用std::size_t((unsigned int)-1),这根本不是一回事——它可能比我们想要的值小很多数量级!

确实,既然我们知道转换都是明确定义的,我们可以测试这个值:

std::cout << (std::size_t(-1) == std::numeric_limits<size_t>::max()) << '\n';
// "1"

而且,为了说明我之前的观点,在我的 64 位系统上:

std::cout << std::is_same<unsigned long, std::size_t>::value << '\n';
std::cout << std::is_same<unsigned long, unsigned int>::value << '\n';
std::cout << std::hex << std::showbase
          << std::size_t(-1) << ' '
          << std::size_t(static_cast<unsigned int>(-1)) << '\n';
// "1"
// "0"
// "0xffffffffffffffff 0xffffffff"

【讨论】:

  • -1 不是文字。它是文字 1 并应用了一元 - 运算符。 (它仍然是int 类型。)
  • @KeithThompson:是的。
  • "建议..." -- 我不确定这是否与 "language-lawyer" 标签一致。
  • 是的,-错误。我的构建日志中的警告为零,因为如果我认为某些警告是可以的,我必须在每次构建时检查每一个警告,并且我可以更好地利用我丰富的空闲时间。
  • @n.m.:很好,但是当第三方标头导致警告时,您会卡住。而且我有比破解其他人的代码来解决它更好的事情要做。当我处于构建新功能的第一阶段时,我还有更好的事情要做,而不是在每次迭代中修复开发分支上的每个未使用的参数警告。
猜你喜欢
  • 2021-03-28
  • 1970-01-01
  • 2022-11-29
  • 2016-10-16
  • 2017-10-19
  • 1970-01-01
  • 2020-05-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多