以下(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
)当sqr64
为x
时(分别为0xFFFFF
,0xFF
,0xFFFFFFFF
)。 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。
我们还有哪些其他选择,在可能更好的性能,可读性和简单性之间取得合理的平衡?
答案 0 :(得分:6)
您已在False
中确定了整数类型别名的基本缺点:它们不包含有关类型转换排名的任何信息。因此,您无法控制这些类型的值是否经过整体促销,并且正如您正确观察到的,当整数提升导致签名类型时,表达式可能具有未定义的行为。
简而言之:您不能使用别名类型来执行通常的算术运算模2 N 。您需要使用其(已知!)转换排名至少为0
的类型。
解决方案通常是将您的操作数转换为<stdint.h>
,int
或unsigned int
的最小值(假设您的平台没有扩展的整数类型),然后计算表达式,然后转换回原始类型(具有正确的模块行为)。在C ++中,您可以编写一个类型特征,以便携方式确定正确的类型。
作为一个更便宜的技巧,再次假设缺少(更宽)扩展的整数类型,你可以将所有内容提升为unsigned long int
,并希望你的编译器以有效的方式进行计算。
答案 1 :(得分:4)
对于较窄的无符号类型,您无法避免对int
进行不可避免的类型提升。
它更多地是乘法运算符的属性而不是其他任何东西。
为了避免未定义的行为极端情况,你可以做的唯一事情是从不使用无符号类型时的乘法,其中最大的平方可以溢出{ {1}}。
幸运的是(除非您在嵌入式世界中工作,否则您可以随时查阅文档以了解具体行为),您可以在很大程度上将int
委托给历史记录:unsigned short
及其int
表弟很可能不会慢,也可能更快。