C 通过强制转换检测未签名的环绕是否为已签名的未定义行为?

C 通过强制转换检测未签名的环绕是否为已签名的未定义行为?,c,casting,undefined-behavior,C,Casting,Undefined Behavior,我正在使用uint16\t作为网络协议中的序列计数器。这个计数器通常按预期的那样缠绕。当一个接收器收到一个数据包时,它会对照最近收到的数据包检查这个计数器,看它是一个新的数据包还是一个无序的数据包 在比较序列号时,需要考虑环绕。因此,例如,如果最后一个序列号是0x4000,则从0x4001到0xbff的序列号较新,从0xC000到0xFFFF和从0x0000到0x3FFF的序列号较小 我目前的做法如下: uint16_t last; uint16_t current; ... // read i

我正在使用
uint16\t
作为网络协议中的序列计数器。这个计数器通常按预期的那样缠绕。当一个接收器收到一个数据包时,它会对照最近收到的数据包检查这个计数器,看它是一个新的数据包还是一个无序的数据包

在比较序列号时,需要考虑环绕。因此,例如,如果最后一个序列号是
0x4000
,则从
0x4001
0xbff
的序列号较新,从
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\u t
,我可以很容易地看出哪个更大,以及增加了多少。因此,例如,如果当前序列号至少比最后一个序列号少5个,即
((int16_t)(current-last)<-5)
,我可以假设这不是由于正常的数据包重新排序和丢弃数据包造成的


我意识到带符号的wrapparound是未定义的,但是在本例中,为了进行比较,我将无符号值视为带符号的。这会调用未定义的行为吗?如果是,那么做这种比较的更好方法是什么?

将使用提升的类型进行减法,除非
int
为16位,在这种情况下,行为是实现定义的

为了安全起见,您可以使用三元函数计算绝对差值:


current>last?当前-上次:上次-当前

超出范围转换的行为由实现定义

你为什么不完全避免这个问题,写下:

if ( current != last && current - last < 0x8000 )
    printf("current is newer\n");
else
    printf("current is older (or same)\n");
if(当前!=last&¤t-last<0x8000)
printf(“当前较新”);
其他的
printf(“当前较旧(或相同)\n”);

注意:此答案仅适用于涉及
uint16\t
的特定问题。对于其他类型,需要使用不同的代码。

您可以将
uint16\u t
s转换为
int32\u t
s并执行减法运算

if (((int32_t) current - (int32_t) last) > 0) {
    printf("current is newer\n");
} else {
    printf("current is older (or same)\n");
}

注意:当强制转换为
int16\u t
时,大于32767的
uint16\u t
s将显示为负(在有符号整数中,最高有效位设置为1表示负一,在无符号类型中,它只是一个常规位)。

只要
INT\u MAX
大于
uint16\u MAX
(在大多数32位或64位平台上为真)不会有任何未定义的行为,因为算术是在提升的类型上完成的。@Quentin:但是如果
INT\u MAX
大于
INT16\u MAX
,操作数将被提升。@EOF和我两天前回答了一个关于这个问题。羞耻感:如果你能腾出字节,就使用
uint64\t
,不要担心包装问题,并希望能活到足够长的时间来见证你的协议包装。根据快速估算,我认为使用64位计数器以每秒1 gB的速度发送1 kB数据包需要500多年的时间。一些通信设备的有效负载非常有限。例如,许多廉价的2.4GHz无线电将数据包的总长度限制在40字节以下,包括前导码、地址和CRC。添加额外的六个字节的序列号可以将有效负载减少20%以上。C11标准草案:
7.20.1.1精确宽度整数类型1 typedef name intN_t指定宽度为N的有符号整数类型,无填充位和2的补码表示。因此,int8_t表示宽度正好为8位的有符号整数类型。[…]
current-last
永远不能为undefined@EOF:意识到,编辑了我的华夫饼干,使你的评论无效,但可能仍然是错误的,大约16位int@MattMcNabb:16位int会发生什么情况?它升级成更大的型号了吗?@Bathsheba:C11。。哦,你现在知道了
6.3.1.3有符号和无符号整数3否则,新类型是有符号的,不能在其中表示值;要么结果是实现定义的,要么发出实现定义的信号。
这种方法的问题在于,它需要右侧的常量取决于无符号变量的实际类型。这在模板中更烦人,在宏中并不总是可行/实用。在
int
为16位的系统上,
current last
将被评估为
uint16\u t
。在
int
为17位或更大的系统上,不计算填充,
当前最后一个
将被计算为(有符号)
int
。对于加法和减法,通常可以通过将总和或差值转换回类型
uint16\u t
来纠正这种情况。在罕见的情况下,要计算两个16位值乘积的16个低位,可能超过46340,应首先将其中一个数字乘以
1u
。给定
uint16\u t x=65533
,则
(uint16_t)(1u*x*x)
的值将为9,因为当x为46341或更大时……65533u*65533==4294574089u和(uint16_t)4294574089u==9,但尝试以
(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如果(x<65000)foo(x),则必须编写它
;x*=1u*x@supercat我不知道你在说什么。“这种情况通常可以补救”——没有什么需要补救的。没有乘法运算here@MattMcNabb:算术运算符将短无符号操作数提升为有符号整数通常不会影响