博客
关于我
用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/

你可能感兴趣的文章
辟谣!湖南大学考试科目不变!不考408!
查看>>
北京理工大学软件学院今年取消招生!
查看>>
这些考研阅卷潜规则你知道几个?
查看>>
【考研英语】考研英语小作文万能模板(致歉信)
查看>>
【数据结构与算法】队列
查看>>
中国最委屈的十所大学
查看>>
【考研经验】2018四跨吉林大学计算机初试复试经验贴(67+72+99+141=379分)
查看>>
8个立竿见影的技巧,帮你摆脱考研焦虑
查看>>
【招生目录和招生简章】华南师范大学 太原理工大学 陕西师范大学 湖南师范大学 西北大学...
查看>>
注意绕道!考研路上几大隐形致命杀手!
查看>>
【20考研】英语第一轮复习要做的二三事
查看>>
阿里某程序员吐槽:每天回家都想着离职,但又舍不得这份薪水
查看>>
【研究生】PyTorch 1.0稳定版正式发布,并向开发者提供免费AI课程
查看>>
考研成绩公布后,这个表情冲上热搜第一!低分就是失败吗?
查看>>
【19调剂】石家庄铁道大学2019年接收硕士研究生调剂公告(非全日制)
查看>>
【19调剂】南京中医药大学关于接收2019年部分专业硕士研究生调剂的通知(四)...
查看>>
平均分392分!某985计算机专硕复试线暴涨!
查看>>
最全的优秀【编程书籍】推荐列表!
查看>>
为何二战考生成功率远远大于应届?
查看>>
哈尔滨工业大学2019初试真题 已更新在GitHub
查看>>