是否将下溢的无符号整数与-1定义良好?

Lig*_*ica 22 c++ language-lawyer unsigned-integer c++11 signed-integer

考虑以下†:

size_t r = 0;
r--;
const bool result = (r == -1);
Run Code Online (Sandbox Code Playgroud)

结果初始化的比较是否result具有明确定义的行为?正如我所期待的那样,
结果true如何?


这个Q&A的写作是因为我不确定有两个因素.
在我的回答中,它们都可以通过使用术语"关键[ly]"来识别.

†此示例的灵感来自于计数器未签名时的循环条件方法:
for (size_t r = m.size() - 1; r != -1; r--)

Kei*_*son 20

size_t r = 0;
r--;
const bool result = (r == -1);
Run Code Online (Sandbox Code Playgroud)

严格来说,价值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_t16位的实现(让我们说它是一个typedeffor unsigned short)并且int是32位.所以SIZE_MAX == 65535和INT_MAX == 2147483647.或者我们可以有32位size_t和64位int.我怀疑任何这样的实现都存在,但标准中没有任何禁止它(见下文).

现在,比较的左侧有类型size_t和值65535.由于signed int可以表示类型的所有值size_t,因此整数提升会将值转换65535 为类型int.==运算符的两侧都有类型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);
Run Code Online (Sandbox Code Playgroud)

要么

const bool result = (r == SIZE_MAX);
Run Code Online (Sandbox Code Playgroud)

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.我几乎可以想象一个int64位的系统,但没有一个对象可以大于2 32 -1个字节,所以size_t是32位.

  • 嗯,是的_almost_我想. (3认同)
  • @KeithThompson:我真的希望C标准委员会能够指定一种声明"包装"无符号类型的方法,这些类型不能隐式转换,也不能升级为签名类型[例如`wrap32_t`和`int`的总和或乘积将是`wrap32_t`无论`int`是16,32还是64位].这样的类型可以编写真正可移植的干净代码,并确保需要可移植性所需的任何类型转换,期间.没有这样的语言功能...... (2认同)

Lig*_*ica 13

是的,结果就是你所期望的.

让我们分解吧.

此时的价值是r多少?好吧,下溢是明确定义的,并且导致在r比较运行时取其最大值.std::size_t没有具体的已知界限,但我们可以对其范围做出合理的假设,与以下方面相比int:

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

而且,只是为了解决这个问题,表达式-1是一元-应用于文字1,并且int在任何系统上都有类型:

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

(我不会引用所有描述如何将anary -应用于a中的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应大于等级long int,等级int应大于等级short int,等级应大于等级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]: 如果目标类型是无符号的,则结果值是与源整数一致的最小无符号整数(模2 n,其中n是用于表示无符号类型的位数). [注意:在二进制补码表示中,此转换是概念性的,并且位模式没有变化(如果没有截断). - 尾注]

[C++11: 4.7/3]:如果目标类型已签名,则如果可以在目标类型(和位字段宽度)中表示该值,则该值不会更改; 否则,该值是实现定义的.

这意味着std::size_t(-1)相当于std::numeric_limits<std::size_t>::max(); 至关重要的是,上述子句中的值n与用于表示无符号类型的位数有关,而不是源类型.否则,我们会这样做std::size_t((unsigned int)-1),这根本不是一回事 - 它可能比我们想要的值小许多个数量级!

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

std::cout << (std::size_t(-1) == std::numeric_limits<size_t>::max()) << '\n';
// "1"
Run Code Online (Sandbox Code Playgroud)

而且,为了说明我之前在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"
Run Code Online (Sandbox Code Playgroud)

  • "*建议......*更合理 - 我不确定这与"语言 - 律师"标签是否一致. (7认同)
  • `-1`不是文字.它是文字"1",应用了一元`-`运算符.(它仍然是`int`类型.) (3认同)
  • 是的, - 错误.在我的构建日志中出现零警告,因为如果我确定某些警告是正常的,我必须在每次构建时检查它们中的每一个,并且我可以更好地利用我丰富的空闲时间. (2认同)