1. 程式人生 > >用float/double作為中轉型別的“雷區”

用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