是否通过强制转换为有符号的未定义行为来检测无符号环绕?
Posted
技术标签:
【中文标题】是否通过强制转换为有符号的未定义行为来检测无符号环绕?【英文标题】:Is detecting unsigned wraparound via cast to signed undefined behavior? 【发布时间】:2015-08-12 13:58:08 【问题描述】:我在网络协议中使用uint16_t
作为序列计数器。该计数器通常按预期回绕。当接收者收到一个数据包时,它会根据最近收到的数据来检查这个计数器,看看它是一个新数据包还是一个乱序数据包。
比较序列号时需要考虑环绕。因此,例如,如果最后一个序列号是0x4000
,那么从0x4001
到0xBFFF
的序列号更新,从0xC000
到0xFFFF
和从0x0000
到0x3FFF
的序列号是更小。
我目前这样做的方式如下:
uint16_t last;
uint16_t current;
...
// read in values for last and current
...
if ((int16_t)(current - last) > 0)
printf("current is newer\n");
else
printf("current is older (or same)\n");
通过将两者相减并将结果视为int16_t
,我可以很容易地看到哪个更大以及多少。因此,例如,如果当前序列号至少比最后一个序列号少 5,即((int16_t)(current - last) < -5)
,我可以假设这不是由于正常的数据包重新排序并丢弃数据包。
我意识到有符号环绕是未定义的,但是在这种情况下,为了进行比较,我将无符号值视为有符号值。这是否会调用未定义的行为,如果是,那么进行此类比较的更好方法是什么?
【问题讨论】:
只要INT_MAX
大于 UINT16_MAX
(在大多数 32 位或 64 位平台上为 true),就不会有任何未定义的行为,因为算术是在提升的类型上完成的。
@Quentin:但是如果INT_MAX
大于INT16_MAX
,操作数将被提升。
@EOF 我两天前回答了一个关于这个的问题。耻辱:
如果您可以节省字节,请使用uint64_t
,不要担心回绕,并希望能够活得足够长,以见证您的协议回绕。通过快速估计,我认为使用 64 位计数器以 1 gB/秒发送 1 kB 数据包需要 500 多年时间。
一些通信设备的有效载荷非常有限。例如,许多廉价的 2.4GHz 无线电将数据包限制为总共少于 40 个字节,包括前导码、地址和 CRC。添加额外的 6 个字节的序列号可以将有用的有效负载减少 20% 以上。
【参考方案1】:
超范围转换的行为是实现定义的。
你为什么不完全避免这个问题并写:
if ( current != last && current - last < 0x8000 )
printf("current is newer\n");
else
printf("current is older (or same)\n");
注意:此答案仅适用于涉及uint16_t
的特定问题。对于其他类型,则需要不同的代码。
【讨论】:
您的方法的问题在于它要求右侧的常量取决于无符号变量的实际类型。这在模板中更烦人,在宏中并不总是可行/实用。 在int
为16 位的系统上,current-last
将被评估为uint16_t
。在 int
为 17 位或更大的系统上,不计算填充,current-last
将被评估为(有符号)int
。对于加法和减法,通常可以通过将总和或差值转换回类型uint16_t
来纠正这种情况。如果想要计算两个 16 位值的乘积的低 16 位可能超过 46,340,则应首先将其中一个数字乘以 1u
。给定uint16_t x=65533;
,(uint16_t)(1u*x*x)
的值将是 9,因为...
...65533u*65533 == 4294574089u 和 (uint16_t)4294574089u == 9,但是当 x 为 46341 或更大时,尝试以 (uint16_t)(x*x)
或 x*x
执行计算可能会导致一些 32 位编译器将所有时间和因果关系的概念抛到了窗外。尽管看起来很疯狂,但一些编译器作者认为,由于标准允许编译器将if (x < 65000) foo(x); x*=x;
转换为foo(x); x*=x;
,而后者“更高效”,他们应该尝试让他们的编译器做到这一点。如果要if (x < 65000) foo(x); x*=x;
,必须写if (x < 65000) foo(x); x*=1u*x;
@supercat 我不确定你在说什么。 “这种情况通常可以得到纠正”——没有什么需要纠正的。这里没有乘法发生
@MattMcNabb:算术运算符将短无符号操作数提升为有符号整数通常如果将结果转换回短无符号值,则不会影响行为,但重要的是任何人依靠此类强制转换,请注意不需要的促销不会影响任何已定义行为但可能导致未定义行为的情况。【参考方案2】:
您可以将uint16_t
s 转换为int32_t
s 并进行减法。
if (((int32_t) current - (int32_t) last) > 0)
printf("current is newer\n");
else
printf("current is older (or same)\n");
注意:当转换为 int16_t
时,大于 32767 的 uint16_t
s 将显示为负数(在有符号整数中,最高有效位设置为 1 表示负一位,在无符号类型中它只是一个常规位)。
【讨论】:
【参考方案3】:除非int
是 16 位,否则减法将使用提升的类型进行,在这种情况下,行为是由实现定义的。
为了安全起见,您可以使用三元来计算绝对差:
current > last ? current - last : last - current
【讨论】:
C11 标准草案:7.20.1.1 Exact-width integer types 1 The typedef name intN_t designates a signed integer type with width N , no padding bits, and a two’s complement representation. Thus, int8_t denotes such a signed integer type with a width of exactly 8 bits.[...]
current - last
永远不能被取消定义
@EOF:意识到,编辑了我的华夫饼,使您的评论无效,但关于 16 位 int 仍然可能是错误的
@MattMcNabb:16 位整数会发生什么?是否提升为更大的类型?
@Bathsheba: C11.. 哦,你现在知道了6.3.1.3 Signed and unsigned integers 3 Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
以上是关于是否通过强制转换为有符号的未定义行为来检测无符号环绕?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 C 或 C++ 标准不明确将 char 定义为有符号或无符号?