謹慎使用 atoi()
剛剛追一個 bug,結果發現問題出在 atoi(),果然還是要用 C++ 才會比較有 type-safety,要不然碰到這類問題,連自己死了都不知道。
事情是這樣子的。在一組透過 HTTP 溝通的網路程式裡,其中一端把某個型別為 uint32_t 的參數,用 sprintf() 轉成文字形式,附在 URL 的 QUERY_STRING 裡,傳送到另一端,接收端收到後,從 URL 裡 parse 出文字形式的這個參數,丟給 atoi() 轉回 uint32_t。整個流程大概如下圖:
uint32_t ID = 0x9fffffff; uint32_t ID = 0x7fffffff;
| ^
| |
sprintf(qstr, "%u", ID); atoi("2684354559");
| |
v |
"2684354559" "2684354559"
| ^
| |
+--(append to URL, pass via Internet)--+
問題出在 atoi() 這個函式,其回傳值是個 int。所以只要這個數字大於 INT_MAX,也就是 0x7FFFFFFF,就一定會回傳 0x7FFFFFFF。於是,這個數字就被改掉了,不正確的結果,導致程式其他部份行為不正常。
解法有兩種:使用 std::istringstream,或是 sscanf(),這兩個都可以反應型別的不同,只不過前者可以自動反應,後者需手動使用 "%u" 格式字串。
如果嫌這兩個方法,寫起來又臭又長,也可以使用 Boost 的 lexical_cast。不過這個一樣得寫明輸出型別為 uint32_t,但比 "%u" 好懂許多。不曉得在 C++0x 裡,可不可以利用 auto,寫成下面那樣,會比較方便:
ID = lexical_cast<auto>("2684354559");
如果 auto 能夠省去,那就更方便了。



18 Comments
我倒是在 itoa 時碰過可能跟 locale/UTF-8 不合的情況,就改用 C++ 了......
int 值域的問題,沒有注意到就爆炸了
那用 atoll 呢?
augustinus
用 C++ 也是會碰到 locale 產生的問題啊,例如我在這篇碰過的問題:《小心繼承 iostream 的邊際效應 (side effect)》。
jeffhung
我沒記錯的話,auto 應該是 compile time 決定的形別,但是你要轉換的字串 "2684354559" 則是 run time 產生的內容,所以應該是辦不到,不知道是不是我弄錯了.
std::istringstream 使用時其實也是要指明型別啊,只是不是直接指定給 istringstream,而是先把變數定出來後,再餵給 istringstream.
我自己是覺得 boost::lexical_cast 比較好用.
jclin,
atoll應付 uint32_t 應該是沒有問題,不過如果碰到 uint64_t,程式還是會出錯。寫得時候小心一點,就不會有問題,但問題是,很難時時刻刻小心,所以如果能有個不用那麼小心也不會出錯的方法,那會比較安全。
jeffhung
av,
不對喔,
"2684354559"雖然內容是 run-time 才知道,但型別在 compile-time 就可以知道了。不過此處的auto跟"2684354559"沒有關係,而是=之 left-hand-side 的型別在 compile-time 已知,所以我會希望如果auto也能使用,那會很棒。augustinus,
itoa 似乎不在 ANSI-C 裡; 我們通常用 sprintf 代替.
jeffhung:
我忘了在那裡回應,用 sstream 的時候我一律都套 en 之類的而不是 C.
路人:
確實我經手過的程式碼也是這麼發展的 itoa -> sprintf -> sstream.
augustinus,
你是指利用
ios::imbue()把 sstream 強制壓在"en"或"C"locale 上嗎?還是說整個程式根本不setlocale(LC_ALL, ""),而是整個設成"en"或"C"locale?後者寫起來可能比較麻煩一些,不過比較不會碰到陷阱;前者我在 VC6 試過,效能拖慢 10 倍,而在非 linux 平台上,搭配 GCC 的 libstdc++ 根本還不支援
ios::imbue()啊。jeffhung
Jeffhung:
我的搭配方法有點繞道而且只適用特定情境,先 std::isprint(foo, locale(bar)) 或 std::isdigit(foo, locale(bar)), 然後就直接 stringstream 了,沒有再 ios::imbue().
以這行 code
ID = lexical_cast("2684354559");
來說,"2684354559" 當然是 compile time 已知的。但你原來的但子並非如此,要被 cast 的字串是透過網路傳輸得到的,並非 compile time 寫好的 "2684354559",因此字串內容當然是 run time 才能確定的。
況且,若是寫 code 時就知道 "2684354559" 了,那就不用 cast 了,直接寫 uint32_t ID = 2684354559 就好了
打錯字了:
s/但子/例子
av,
不是啊,
"2684354559"的「型別」在 compile-time 已知,只要再知道 right-hand-side 的型別,就足夠 compiler 決定lexical_cast該怎麼具現化。jeffhung
"2684354559" 的型別可能是 char*,但它將被 cast 的型別則未知,端看 programmer 如何寫。例:
char *s = "2684354559"
lexical_cast(s);
lexical_cast(s);
lexical_cast(s);
lexical_cast(s);
lexical_cast(s); // and throws bad_lexical_cast
compiler 不可能知道你想把 s cast 成什麼東西啊. 比較可行的寫法應該是:
auto ID = lexical_cast(s);
尖角括號裡的東西都被清掉了 >_< 重寫一次:
"2684354559" 的型別可能是 char*,但它將被 cast 的型別則未知,端看 programmer 如何寫。例:
char *s = "2684354559"
lexical_cast<int>(s);
lexical_cast<short>(s);
lexical_cast<double>(s);
lexical_cast<uint_32>(s);
lexical_cast<any_type>(s); // and throws bad_lexical_cast
compiler 不可能知道你想把 s cast 成什麼東西啊. 比較可行的寫法應該是:
auto ID = lexical_cast<uint_32>(s);
av,
所以我說:「…這個一樣得寫明輸出型別為
uint32_t…」XD至於
auto,我覺得如果可以用這樣的寫法,那就太好了:uint32_t ID = lexical_cast<auto>("1234567");甚至最好是:
template <class From, class To = auto> To lexical_cast(From s) {...} uint32_t ID = lexical_cast("1234567");Jeff Hung
可是我就是說 lexical_cast<auto>(\"1234567\") 不可行啊,至少 lexical_cast<auto>(s) 不可行。
請問 compiler 該把 auto 的型別推導為 uint_32 或是 float? 要考慮到 s 的內容可能是 \"123456\" 或是 \"123.456\".
lexical_cast 的 template 參數是無法這樣推導的,必需寫明,而 auto 頂多只能用在我前面講的情況:
auto ID = lexical_cast<unit_32>(s)
我想也許我會錯你的意思了,你在 comment #16 的意思是不是說等號左邊的的型別已定為 uint_32, 所以等號右邊的型別可以是 auto, 因此 lexcial_cast 的 template 參數可以用 auto 甚至不用填? 我認為即便如此仍然是不可行的,因為等號右邊的 lexcial_cast() 會先求值,而且再傳給等號左邊的 ID 時,兩邊可以是不同型別,只能要轉換就行了. 但若是照我的寫法,ID 的型別為 auto, 這是 auto 一般的用法,這樣應該較有機會被推導為 uint_32.
Post a Comment