如何在固定宽度类型上强制无符号算术?

时间:2016-11-25 10:42:14

标签: c++ c c99 standards-compliance

以下(C99和更新版本)代码想要计算一个正方形,限制为与原始固定宽度类型相同的位数。

    #include <stdint.h>
     uint8_t  sqr8( uint8_t x) { return x*x; }
    uint16_t sqr16(uint16_t x) { return x*x; }
    uint32_t sqr32(uint32_t x) { return x*x; }
    uint64_t sqr64(uint64_t x) { return x*x; }

问题是:取决于int大小,可以对提升为(signed)int的参数执行一些乘法,结果溢出(signed)int,因此就标准而言是未定义的结果;并且可以想象错误的结果,特别是在(越来越罕见的)没有使用two's complement的机器上。

如果int是32位(分别为16位,64位,80位或128位),则会发生sqr16(分别为sqr8,{{ 1 {},sqr32)当sqr64x时(分别为0xFFFFF0xFF0xFFFFFFFF)。 4个功能都不能在C99下正式移植!!

C11或更高版本,或某些版本的C ++,是否解决了这种不幸的情况?

一个简单,有效的解决方案是:

0xFFFFFFFFFFFFFFFF

这符合标准,因为 #include <stdint.h> uint8_t sqr8( uint8_t x) { return 1u*x*x; } uint16_t sqr16(uint16_t x) { return 1u*x*x; } uint32_t sqr32(uint32_t x) { return 1u*x*x; } uint64_t sqr64(uint64_t x) { return 1u*x*x; } 未升级为1u且仍未签名;因此,左乘法,然后是右乘,则作为无符号执行,因此被很好地定义以在必要数量的低位中产生正确的结果;对于结果宽度的最终隐式强制转换也是如此。

更新:正如comment by Marc Glisse中的建议,我尝试了这个变体有八个编译器(三个版本的GCC用于x86,从3.1开始,MS C / C ++ 19.00,Keil ARM编译器5,两个用于ST7变体的Cosmic编译器,Microchip MCC18)。它们都生成了与原始代码完全相同的代码(我在实际项目的发布模式中使用了优化)。但是,编译器可能会产生比原始代码更糟糕的代码;我还有其他几个嵌入式编译器,包括一些68K和PowerPC。

我们还有哪些其他选择,在可能更好的性能,可读性和简单性之间取得合理的平衡?

2 个答案:

答案 0 :(得分:6)

您已在False中确定了整数类型别名的基本缺点:它们不包含有关类型转换排名的任何信息。因此,您无法控制这些类型的值是否经过整体促销,并且正如您正确观察到的,当整数提升导致签名类型时,表达式可能具有未定义的行为。

简而言之:您不能使用别名类型来执行通常的算术运算模2 N 。您需要使用其(已知!)转换排名至少为0的类型。

解决方案通常是将您的操作数转换为<stdint.h>intunsigned int的最小值(假设您的平台没有扩展的整数类型),然后计算表达式,然后转换回原始类型(具有正确的模块行为)。在C ++中,您可以编写一个类型特征,以便携方式确定正确的类型。

作为一个更便宜的技巧,再次假设缺少(更宽)扩展的整数类型,你可以将所有内容提升为unsigned long int,并希望你的编译器以有效的方式进行计算。

答案 1 :(得分:4)

对于较窄的无符号类型,您无法避免对int进行不可避免的类型提升。

它更多地是乘法运算符的属性而不是其他任何东西。

为了避免未定义的行为极端情况,你可以做的唯一事情是从不使用无符号类型时的乘法,其中最大的平方可以溢出{ {1}}。

幸运的是(除非您在嵌入式世界中工作,否则您可以随时查阅文档以了解具体行为),您可以在很大程度上将int委托给历史记录:unsigned short及其int表弟很可能不会慢,也可能更快。