strtoul 関数の不思議 C 言語標準の strtoul 関数は入力文字列がマイナスで始まっていてもエラーになりません。例えば "-500" という文字列を渡すと ULONG_MAX-499 が返ってきます。これはライブラリのバグではなく仕様に従った動作のようです。仕様の意図がわかりません。
C 言語のプログラムで "100" のように整数を表す文字列を 100 という数値に変換したい場合、 atoi 関数を使うのが簡単です。しかし、 atoi 関数は文字列の表す整数が int 型で表現可能な範囲を超えると結果が不定になるので、オーバーフローを検出したい場合には、別に検査を入れるか、検査も変換も自分でするか、あるいは strtol 関数などオーバーフロー時の動作が決まっている関数を使うことになります (ちなみに sscanf は atoi と同様、オーバーフローの場合の動作が決まっていません)。
オーバーフローを検出する必要があって、しかも負の数を扱う必要がない場合、これまで僕は unsigned long 型を返す strtoul 関数を使ってきました。例えば、 0 以上 10000 以下の整数以外はエラーとする場合、次のように書いていました。
プログラム 1 (抜粋; 全体)
char *s; ... /* s に整数を表す文字列をセット */ char *end; unsigned long ul = strtoul(s, &end, 10); if (*s == '\0' || *end != '\0' || ul > 10000) { ... /* エラー時の処理 */ } int n = (int)ul;
このコードは、「普通の」入力に対しては、正しく動作します (32 ビット Windows 上の Cygwin で確認)。
0 から 10000 までの整数を入力してください: 100 入力された整数は 100 0 から 10000 までの整数を入力してください: 10000 入力された整数は 10000 0 から 10000 までの整数を入力してください: 100x エラー 0 から 10000 までの整数を入力してください: 10001 エラー 0 から 10000 までの整数を入力してください: -1 エラー 0 から 10000 までの整数を入力してください: 4294967296 ← ULONG_MAX より 1 大きい数 エラー 0 から 10000 までの整数を入力してください: ← 何も入力しないで Enter エラー
上のコードでは、文字列 s の先頭の空白文字は無視されますが、文字列の末尾に空白文字があったらエラーになります。あまり直感的ではないので、その意味では良くないコードかもしれません。
0 から 10000 までの整数を入力してください: ␣␣␣100 ← 先頭に空白あり 入力された整数は 100 0 から 10000 までの整数を入力してください: 100␣␣␣ ← 末尾に空白あり エラー 0 から 10000 までの整数を入力してください: ␣␣␣100␣␣␣ ← 先頭にも末尾にも空白あり エラー
先頭に空白文字がある場合もエラーにしたい場合は、 isspace(*s) ならエラーになるようにすればよいわけで、何も難しいことはありません。
ここで素直に s の先頭が数字かどうか調べるようにしていれば、問題は起きなかったのですが……そうしなかった結果、予想外の動作をする場合が出てきてしまいました。
0 から 10000 までの整数を入力してください: -4294967196 ← マイナスの後に ULONG_MAX - 99 入力された整数は 100
何ですと。
strtoul に与えた文字列がマイナス記号で始まる場合にエラーにならないというだけでも僕には驚きでした。しかし、考えてみると "-0" の場合にはエラーにならなくても理解できるかもしれません。しかし、 "-1" で ULONG_MAX が返ってきたり、 "-500" で ULONG_MAX-499 が返ってきたりすることで、どういうメリットがあるというのか、僕にはさっぱりわかりません。それでいて、 "-9999999999" のようにマイナスの後に ULONG_MAX より大きな値を指定すると、マイナスがない場合と同じように、 ULONG_MAX が返ってきて errno が ERANGE に設定されると。もはや何が何だか。
C99 標準 (PDF) 7.20.1.4 節や Open Group Base Specifications 2004 (POSIX) を調べると、確かに文字列がマイナスで始まってもよいことになっています。つまり、これはライブラリのバグではなく仕様通りの動作です。 GNU libc のマニュアルにはわざわざ文字列が負の数を表す場合の動作について注意書きが付いています。この仕様は、細かい動作をきちんと調べずにプログラムを書く者への嫌がらせでしょうか。
strtoul ではなく strtol を使って次のように書けば、問題は起きません。
プログラム 2 (抜粋; 全体)
char *s; ... /* s に整数を表す文字列をセット */ char *end; long l = strtol(s, &end, 10); if (*s == '\0' || *end != '\0' || l < 0 || l > 10000) { ... /* エラー時の処理 */ } int n = (int)l;
いったいどういう意図があってこんな仕様になっているのか、誰か知っている人がいたら教えてもらえれば幸いです。
この文章を書いた後で知ったのですが、プログラマーの高木信尚さんがブログ「標準 C ライブラリの実装」の中で strtoul 関数の実装を示していて、そのページのコメント欄で、 strtoul 関数の仕様でのマイナス記号の取り扱いが話題に上っていました。僕はそこの「(返却値の型で) 負数化したものとする」 (negated (in the return type)) というのを「単項演算子 - を適用したのと同じ結果を返す」という意味だと解釈しましたが、それ以外の解釈の可能性は検討していませんでした。仕様書を読むのは素人には難しいです。