博客
关于我
用float/double作为中转类型的“雷区”
阅读量:274 次
发布时间:2019-03-03

本文共 4401 字,大约阅读时间需要 14 分钟。

n由于lua用double作为number类型的底层数据中转类型。而实际应用中多以int类型作为函数调用的参数(特别是C实现的API)。因而,double/int/unsigend int之间的数值转换在接入lua的项目中应用十分广泛。

实际项目发现,double/int/unsigend int之间的数值转换存在一个严重且极容易被忽视的”雷区”

根据IEEE二进制浮点数算术标准(IEEE 754)定义,浮点数只是一个近似值。

测试原由:
近日发现一个奇葩的问题,在lua中,传一个大于INT_MAX的整数给lua,或者在C++中用最高位为1的unsigned int u 采用如下方式返回值 Lua_PushNumber(L, u);
lua将产生一个“异常状态的nunmber对象”,对该对象执行%X取值可以取到正确的十六进制值,执行%d取值,将只能取到-2147483648(0x80000000)
更让人纠结的是这个现象只发生在linux下,windows下是正常的,大于INT_MAX的值%d提取将取到对应的负数值,在需要的地方传值给对应的unsigned int,仍然是正确的值。
看到这个现象,第一反应是lua本身的bug,于是研究了lua的源码,发现lua除了采用double存储和传递number对象,没有其他不规矩操作。
而lua在%X和%d对number取值时执行的操作分别如下:
((int)luaL_check_number(L, n)) //%d
((unsigned int)luaL_check_number(L, n)) //%X
于是怀疑到C++double类型到int和unsigned int类型转换出了问题,于是写下了如下测试代码:

以下是测试代码和测试结果

func TestFloat1(t *testing.T) {    tt := []uint32{        0x7FFFFFFE,        0x7FFFFFFF,        0x80000000,        0x80000001,        0xFF000000,    }    for _, u := range tt {        oki := int32(u)        f := float64(u)        fi := int32(f)        err := oki != fi        fmt.Printf("x=0x%08X u=%10d oki=%11d f=%12.1f fi=%11d  err=%v\n",            u, u, oki, f, fi, err)    }    //x=0x7FFFFFFE u=2147483646 oki= 2147483646 f=2147483646.0 fi= 2147483646  err=false    //x=0x7FFFFFFF u=2147483647 oki= 2147483647 f=2147483647.0 fi= 2147483647  err=false    //x=0x80000000 u=2147483648 oki=-2147483648 f=2147483648.0 fi=-2147483648  err=false    //x=0x80000001 u=2147483649 oki=-2147483647 f=2147483649.0 fi=-2147483648  err=true    //x=0xFF000000 u=4278190080 oki=  -16777216 f=4278190080.0 fi=-2147483648  err=true}func TestFloat2(t *testing.T) {    tt := []float64{        0x7FFFFFFE,        0x7FFFFFFF,        -1,        -2,        0x80000000,        0x80000001,        0x80000002,        0x880000002,        0xFF000000,        0xFFFFFFFE,        0xFFFFFFFF,    }    for _, f := range tt {        fi := int32(f)        u := uint32(f)        oki := int32(u)        err := fi != oki        fmt.Printf("x=0x%08X f=%13.1f u=%10d fi=%11d oki=%11d err=%v\n",            u, f, u, fi, oki, err)    }    //x=0x7FFFFFFE f= 2147483646.0 u=2147483646 fi= 2147483646 oki= 2147483646 err=false    //x=0x7FFFFFFF f= 2147483647.0 u=2147483647 fi= 2147483647 oki= 2147483647 err=false    //x=0xFFFFFFFF f=         -1.0 u=4294967295 fi=         -1 oki=         -1 err=false    //x=0xFFFFFFFE f=         -2.0 u=4294967294 fi=         -2 oki=         -2 err=false    //x=0x80000000 f= 2147483648.0 u=2147483648 fi=-2147483648 oki=-2147483648 err=false    //x=0x80000001 f= 2147483649.0 u=2147483649 fi=-2147483648 oki=-2147483647 err=true    //x=0x80000002 f= 2147483650.0 u=2147483650 fi=-2147483648 oki=-2147483646 err=true    //x=0x80000002 f=36507222018.0 u=2147483650 fi=-2147483648 oki=-2147483646 err=true    //x=0xFF000000 f= 4278190080.0 u=4278190080 fi=-2147483648 oki=  -16777216 err=true    //x=0xFFFFFFFE f= 4294967294.0 u=4294967294 fi=-2147483648 oki=         -2 err=true    //x=0xFFFFFFFF f= 4294967295.0 u=4294967295 fi=-2147483648 oki=         -1 err=true}

结论如下:

1. 无论在linux还是在windows下,将一个超出int值域范围[-2147483648,2147483647]的doulbe值,转换为int时,将只能取到-2147483648(0x80000000)
2. 将一个超出超出unsigned int值域范围[0, 4294967295]的double类型,转换为unsigned int,将安全的取到对应16进制值的低32位
3. windows优先将常量表达式计算为int,linux优先将常量表达式结果计算为unsigned int(不知为何,这个差异在这个测试用例中没能体现出来)
4. (int)doubleValue操作在C++中是极度危险的“雷区”,应当在编码规范层次严格禁止。
5. (unsigned int)doubleValue操作在C++中是安全的
6. 想从double得到int,必须使用(int)(unsigned int)doubleValue这样的操作

经验教训:

由于lua采用double存储和传递number对象,这个问题必须得到重视,并且需要在编码规范的层次,严格禁止这种unsigned int->double, double->int的行为

在C++代码中大量使用的如下操作将是危险的:

1. int nIntValue = (int)Lua_ValueToNumber(L, 1); //Danger!!! 对不在int范围内的number,只能取到-2147483648(0x80000000)
2. Lua_PushNumber(L, unsignedIntValue); //Danger!!!如果unsignedIntValue最高位为1,将产生一个超出int范围的异常number对象

以上两种用法必须修改为

1. int nIntValue = (int)(unsigned int)Lua_ValueToNumber(L, 1);
2. Lua_PushNumber(L, (int)unsignedIntValue);

以下结论必须在日常编码中引起重视:

1. (int)doubleValue操作在C++中是极度危险的“雷区”,应当在编码规范层次严格禁止。
2. (unsigned int)doubleValue操作在C++中是安全的
3. int/unsigned int相互转换是安全的
3. 想从double得到int,必须使用(int)(unsigned int)doubleValue这样的操作
4. 无论在linux还是在windows下,将一个超出int值域范围[-2147483648,2147483647]的doulbe值,转换为int时,将只能取到-2147483648(0x80000000)
5. 将一个超出超出unsigned int值域范围[0, 4294967295]的double类型,转换为unsigned int,将安全的取到对应16进制值的低32位
6. windows优先将常量表达式计算为int,linux优先将常量表达式结果计算为unsigned int(不知为何,这个差异在这个测试用例中没能体现出来)

我将以上测试代码放在这里:

参考资料:

IEEE二进制浮点数算术标准(IEEE 754)

转载地址:http://gcjl.baihongyu.com/

你可能感兴趣的文章
2021-ICPD昆明站-I Mr. Main and Windmills
查看>>
Tips - 0712
查看>>
iOS-编译报错duplicate symbol _OBJC_IVAR
查看>>
animation跑马灯动画实现两种方法
查看>>
计时器模仿地球绕太阳圆周运动
查看>>
fpga工程师笔试题
查看>>
201604-4 游戏 ccf
查看>>
1144. The Missing Number (20)
查看>>
为什么阿里巴巴不建议在for循环中使用”+”进行字符串拼接
查看>>
【Spring Boot 26】分别在SpringBoot和Vue中解决跨域问题
查看>>
Class.forName(),classloader.loadclass用法详解
查看>>
tp5.1 页面错误!请稍后再试~ 安装好后,提示错误
查看>>
阿里云 安全组规则 设置某个IP不能访问服务器(出站)
查看>>
禁止重复提交(JavaScript控制表单…
查看>>
php js 通过sotitle(id,arr)函数输入ID取得返回值
查看>>
删除外键约束
查看>>
c++ 预处理命令 #error 用法
查看>>
OpenGL fragmentlist片段列表的实例
查看>>
Qt Creator编码
查看>>
Qt Designer的UI文件格式
查看>>