HN 日本語サマリー

← 一覧へ戻る
プログラミング

標準Cで文字列から浮動小数点数への変換エラーを検出するポータブルな方法はない

No portable way to detect string-to-float conversion errors in standard C (sebsite.pw)

6 pointsby signa110 コメント

要約

この記事は、標準C言語における文字列から浮動小数点数への変換関数(strtodなど)のエラー検出の複雑さと、ポータブルな方法が存在しない可能性について解説しています。標準Cの仕様では、オーバーフローやアンダーフロー時のエラー報告が実装定義であったり、そもそもエラー報告が必須でなかったりするため、errnoや浮動小数点例外フラグだけでは確実にエラーを検出できないことが示されています。POSIX仕様との違いや、未定義の挙動についても言及されています。

全文翻訳

ああ、標準Cで文字列から浮動小数点数への変換エラーをポータブルにチェックする方法はないらしい。 2026-10-09 これは、以前の投稿の続編のようなものです。以前の投稿では、math_errhandlingマクロと、glibcおよびmuslでの浮動小数点エラー処理(および標準での指定方法)について説明しました。要約すると、math_errhandlingは、math.h関数のどのエラー処理メカニズムがサポートされているか(errno (MATH_ERRNO) および/または浮動小数点例外 (MATH_ERREXCEPT))を示すマクロです。 以前の投稿で触れなかったことが一つあります。math_errhandlingの影響を受けるが、math.hには含まれていない関数のファミリーがあります。それは文字列から浮動小数点数への変換関数、strtod、strtof、strtold、strtod32、strtod64、strtod128です。 7.25.2.6p12: 正しい値がオーバーフローし、デフォルトの丸め処理が有効な場合(7.12.2)、戻り値の型と値の符号に応じて、HUGE_VAL、HUGE_VALF、またはHUGE_VALLの正または負の値が返されます。整数式 math_errhandling & MATH_ERRNO がゼロでない場合、整数式 errno は ERANGE の値になります。整数式 math_errhandling & MATH_ERREXCEPT がゼロでない場合、「overflow」浮動小数点例外が生成されます。 結果がアンダーフローした場合(7.12.2)、関数は戻り値の絶対値が、戻り値の型で正規化された最小の正数以下の値を返します。整数式 math_errhandling & MATH_ERRNO がゼロでない場合、errno が ERANGE の値になるかどうかは実装定義です。整数式 math_errhandling & MATH_ERREXCEPT がゼロでない場合、「underflow」浮動小数点例外が生成されるかどうかは実装定義です。 これらの関数のLinuxのmanページには、このことが全く記載されていません。そして、それには理由があります。標準が言っているのは、math_errhandlingがerrnoのサポートを広告していない場合(例えばmuslの場合)、文字列から浮動小数点数への変換関数はエラー時にerrnoを設定しないということです。さらに、結果がアンダーフローした場合、関数はエラーを報告する必要すらありません。 戻り値だけではエラーが発生したかどうかを判断するには不十分であることを念頭に置いてください。オーバーフローをテストするには、2つのエラー処理メカニズムのいずれかを使用する必要があります。 以下は、文字列から浮動小数点数への変換関数におけるオーバーフロー/アンダーフローエラーをポータブルにチェックするための、標準に準拠した方法の試みです。 feclearexcept(FE_OVERFLOW | FE_UNDERFLOW); erro = 0; double x = strtod(s, nullptr); bool errored = math_errhandling & MATH_ERRNO ? errno == ERANGE : fetestexcept(FE_OVERFLOW | FE_UNDERFLOW); ただし、これはアンダーフローが検出されることを保証するものではありません。なぜなら、アンダーフローの報告は実装にとって完全に任意だからです。 あなたがこれまでにこれを一度も行ったことがない理由(そしてmanページにこれが記載されていない理由)は、POSIXがこれらの関数を異なる方法で指定しているからです。 「正しい値が表現可能な値の範囲外にある場合、±HUGE_VAL、±HUGE_VALF、または±HUGE_VALL(値の符号に応じた)が返され、errnoは[ERANGE]に設定されます。正しい値がアンダーフローを引き起こす場合、戻り値の型で表現可能な最小の正規化された正数以下の絶対値を持つ値が返され、errnoは[ERANGE]に設定されます。」 したがって、POSIXはmath_errhandlingを気にしません。常にerrnoを設定することを要求します。オーバーフローまたはアンダーフローが発生した場合。 POSIXが標準Cよりも関数に厳しい要件を指定することは珍しくありませんが、manページがこれを拡張機能として全く言及していないという事実は(strtod(3)もPOSIX仕様のstrtod(3p)も)、私にとって非常に注目に値します。 POSIXの動作が標準Cと互換性があるかどうかは…不明です。少なくともmath.h関数については、math_errhandlingの値に関係なくerrnoを設定することが許可されています。 7.12.2p8: ドメインエラー、ポールエラー、またはレンジエラーが発生し、整数式 math_errhandling & MATH_ERRNO がゼロの場合、errno はエラーに対応する値に設定されるか、または変更されないままになります。 しかし、以前、errno.hを仕様する際に、標準は次のように述べています。 7.5p3: [...] errno の値は、エラーがない場合でも、このドキュメントの説明で errno の使用が文書化されていない限り、ライブラリ関数呼び出しによってゼロ以外に設定される可能性があります。errno はこれらの関数の説明で文書化されており、その説明では math_errhandling & MATH_ERRNO がゼロの場合に errno を設定することについては言及されていません。 したがって、POSIXの動作は標準に準拠していないことを示唆しています。 しかし、待ってください!これまでに話してきたことは、オーバーフローとアンダーフローについてだけです。しかし、文字列が不正な形式で、数値として解析できない場合、標準はエラーを全く指定していません。 7.25.2.6p11: 関数は、変換された値を返します。変換が実行されなかった場合、正または符号なしのゼロが返されます。 代わりに、endptrパラメータを使用し、後でendptr == nptr(つまり、終了ポインタが開始ポインタと同じで、データが解析されなかったことを意味します)を確認する必要があります。 7.25.2.6p8: 対象のシーケンスが空であるか、期待される形式でない場合、変換は実行されません。endptr がヌルポインタでない場合、nptr の値が endptr が指すオブジェクトに格納されます。 manページ strtod(3) も同様のことを述べています。 「変換が実行されなかった場合、ゼロが返され、(endptr がヌルの場合を除き)nptr の値が endptr によって参照される場所に格納されます。」 しかし、POSIXを見ると次のようになっています。 「正常に完了した場合、これらの関数は変換された値を返します。変換が実行されなかった場合、0 が返され、errno は [EINVAL] に設定される場合があります。」 「場合があります」という言葉は、実装定義であることを意味します。 しかし、これは大きな問題です。なぜなら、エラーをチェックするために次のようなことを行うのは一般的だからです。 erro = 0; double x = strtod(s, nullptr); if (errno != 0) { // ... } strtod(3) はまさにこれを行うことを提案しています。 「成功と失敗の両方で合法的にゼロが返される可能性があるため、呼び出しプログラムは呼び出しの前に errno を 0 に設定し、その後 errno がゼロ以外の値になったかどうかをチェックしてエラーが発生したかどうかを判断する必要があります。」 しかし、POSIX互換のlibcであっても、これはポータブルではありません!異なる準拠libcは、変換が実行されなかった場合に異なる動作をする可能性があります。 実際… erro = 0; strtod("x", nullptr); printf("%d\n", errno); glibcでは、これは0を出力します。なぜなら、glibcのstrtodはerrnoをEINVALに設定しないからです。一方、muslはerrnoをEINVALに設定するため、これは「22」を出力します。 これはLinuxのmanページでは全く文書化されていません。また、私の標準の読み方によれば、標準Cに準拠していません。なぜなら、他の文書化されたエラー条件を持つ関数でerrnoをゼロ以外の値に設定しているからです(そして標準Cの限りでは、不正な入力はエラー条件ではありません)。 muslは、math.h関数でerrnoを設定しないため、strtodの説明におけるerrnoの条件はもはや適用されず、したがってerrnoの文書化された使用法はない(errnoの使用はmath_errhandlingの値に依存するため)と主張して自身を守ることができます。しかし、それは明らかに無理な主張です。 いずれにしても、標準ではこの部分の記述がより明確になる必要があることは明らかです。 結論 楽しみのために、文字列から浮動小数点数への変換関数でエラーをチェックするすべての「正しい」方法のリストで締めくくることにしました。これでポイントが強調されるはずです。 オーバーフロー POSIXをターゲットにしている場合は、呼び出しの前に errno を 0 に設定し、その後 copysign(result, 1.0) == HUGE_VAL && errno == ERANGE をチェックします。それ以外の場合は、呼び出しの前に errno を 0 に設定し、feclearexcept(FE_OVERFLOW) を呼び出し、その後 copysign(result, 1.0) == HUGE_VAL の場合は、math_errhandling の値に応じて、errno == ERANGE または fetestexcept(FE_OVERFLOW) をチェックします。 特定の1つの実装をターゲットにしており、その実装がサポートするエラー報告方法を事前に知っている場合は、math_errhandling のチェックをスキップして、2つの方法のうち1つだけをサポートできます。 feclearexcept/fete を使用している場合…