用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章