用float/double作为中转类型的“雷区”

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(不知为何,这个差异在这个测试用例中没能体现出来)

我将以上测试代码放在这里:
https://github.com/vipally/glab/blob/master/lab6/lab6_test.go

参考资料:
IEEE二进制浮点数算术标准(IEEE 754) http://zh.wikipedia.org/wiki/IEEE_754

猜你喜欢

转载自blog.csdn.net/vipally/article/details/53148701
今日推荐