oh, apparently it's not possible to portably check for string-to-float conversion errors in standard c

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

oh, apparently it’s not possible to portably check for string-to-float conversion errors in standard c 2026-10-09 this is kinda a sequel-ish to my previous post, in which i go over the math_errhandling macro, and how math error handling is done in glibc and musl (as well as how it’s specified in the standard).

哦,显然在标准 C 中无法可移植地检查字符串转浮点数的错误。2026-10-09,这算是上一篇文章的续篇,我在那篇文章中讨论了 math_errhandling 宏,以及 glibc 和 musl 是如何处理数学错误的(以及标准中是如何规定的)。

to summarize: math_errhandling is a macro which indicates which error-handling mechanisms are supported by math.h functions: errno (MATH_ERRNO) and/or floating-point exceptions (MATH_ERREXCEPT). there’s one thing i didn’t mention in my previous post: there’s one family of functions which is affected by math_errhandling but which isn’t in math.h: the string to float conversion functions strtod, strtof, strtold, strtod32, strtod64, and strtod128:

总结一下:math_errhandling 是一个宏,用于指示 math.h 函数支持哪些错误处理机制:errno (MATH_ERRNO) 和/或浮点异常 (MATH_ERREXCEPT)。我在上一篇文章中漏掉了一点:有一个函数族受 math_errhandling 影响,但它们并不在 math.h 中,那就是字符串转浮点数函数 strtod、strtof、strtold、strtod32、strtod64 和 strtod128:

7.25.2.6p12: If the correct value overflows and default rounding is in effect (7.12.2), plus or minus HUGE_VAL, HUGE_VALF, or HUGE_VALL is returned (according to the return type and sign of the value); if the integer expression math_errhandling & MATH_ERRNO is nonzero, the integer expression errno acquires the value of ERANGE; if the integer expression math_errhandling & MATH_ERREXCEPT is nonzero, the “overflow” floating-point exception is raised.

7.25.2.6p12:如果正确值溢出且默认舍入生效 (7.12.2),则返回正或负的 HUGE_VAL、HUGE_VALF 或 HUGE_VALL(取决于返回类型和值的符号);如果整数表达式 math_errhandling & MATH_ERRNO 非零,则整数表达式 errno 获取 ERANGE 的值;如果整数表达式 math_errhandling & MATH_ERREXCEPT 非零,则会引发“溢出”浮点异常。

If the result underflows (7.12.2), the functions return a value whose magnitude is no greater than the smallest normalized positive number in the return type; if the integer expression math_errhandling & MATH_ERRNO is nonzero, whether errno acquires the value ERANGE is implementation-defined; if the integer expression math_errhandling & MATH_ERREXCEPT is nonzero, whether the “underflow” floating-point exception is raised is implementation-defined.

如果结果下溢 (7.12.2),函数将返回一个量级不大于返回类型中最小规范化正数的值;如果整数表达式 math_errhandling & MATH_ERRNO 非零,errno 是否获取 ERANGE 值由实现定义;如果整数表达式 math_errhandling & MATH_ERREXCEPT 非零,是否引发“下溢”浮点异常由实现定义。

the linux man pages for these functions literally don’t mention this at all, and there actually is a reason why which i’ll get to in a sec, but what the standard is saying is, if math_errhandling doesn’t advertise support for errno (as is the case on musl, for instance), the string to float conversion functions don’t set errno on error. furthermore, if the result underflows, the function isn’t required to report an error at all.

这些函数的 Linux 手册页完全没有提到这一点,这其实是有原因的,我稍后会讲到。但标准的意思是,如果 math_errhandling 没有声明支持 errno(例如在 musl 上就是这种情况),那么字符串转浮点数函数在出错时不会设置 errno。此外,如果结果下溢,函数根本不需要报告错误。

keep in mind that the return value alone isn’t enough to determine if an error occurred, so to test for overflow, you need to use one of the two error handling mechanisms. here is my attempt at a standards-blessed way of portably checking for overflow/underflow errors in the string to float functions: feclearexcept(FE_OVERFLOW | FE_UNDERFLOW); errno = 0; double x = strtod(s, nullptr); bool errored = math_errhandling & MATH_ERRNO ? errno == ERANGE : fetestexcept(FE_OVERFLOW | FE_UNDERFLOW);

请记住,仅凭返回值不足以确定是否发生了错误,因此要测试溢出,你需要使用两种错误处理机制之一。这是我尝试的一种符合标准、可移植地检查字符串转浮点数函数中溢出/下溢错误的方法:feclearexcept(FE_OVERFLOW | FE_UNDERFLOW); errno = 0; double x = strtod(s, nullptr); bool errored = math_errhandling & MATH_ERRNO ? errno == ERANGE : fetestexcept(FE_OVERFLOW | FE_UNDERFLOW);

note that this still doesn’t guarantee that underflow is detected, since reporting underflow is entirely optional for the implementation. the reason you’ve never done this ever (and the reason the man pages don’t mention this) is that posix specifies the functions differently:

请注意,这仍然不能保证检测到下溢,因为报告下溢对于实现来说是完全可选的。你从未这样做过(以及手册页没有提到这一点)的原因是 POSIX 对这些函数的规定不同:

If the correct value is outside the range of representable values, ±HUGE_VAL, ±HUGE_VALF, or ±HUGE_VALL shall be returned (according to the sign of the value), and errno shall be set to [ERANGE]. If the correct value would cause an underflow, a value whose magnitude is no greater than the smallest normalized positive number in the return type shall be returned and errno set to [ERANGE].

如果正确值超出了可表示值的范围,则应返回 ±HUGE_VAL、±HUGE_VALF 或 ±HUGE_VALL(取决于值的符号),并且应将 errno 设置为 [ERANGE]。如果正确值会导致下溢,则应返回一个量级不大于返回类型中最小规范化正数的值,并将 errno 设置为 [ERANGE]。

so posix doesn’t give a shit about math_errhandling; it always requires the implementation to set errno if overflow or underflow occurs. while it’s not uncommon for posix to specify stricter requirements on functions than standard c, the fact that the man page never makes any note of this being an extension (neither strtod(3) nor the posix specification strtod(3p)) is really notable to me.

所以 POSIX 根本不在乎 math_errhandling;它总是要求实现在发生溢出或下溢时设置 errno。虽然 POSIX 对函数的要求比标准 C 更严格并不罕见,但手册页从未注明这是一个扩展(无论是 strtod(3) 还是 POSIX 规范 strtod(3p))这一事实对我来说非常值得注意。

whether or not posix’s behavior is even compatible with standard c is… unclear. at least for math.h functions, setting errno regardless of the value of math_errhandling is permitted: 7.12.2p8: If a domain, pole, or range error occurs and the integer expression math_errhandling & MATH_ERRNO is zero, then errno shall either be set to the value corresponding to the error or left unmodified.

POSIX 的行为是否与标准 C 兼容……尚不清楚。至少对于 math.h 函数,无论 math_errhandling 的值如何,设置 errno 都是允许的:7.12.2p8:如果发生定义域、极点或范围错误,且整数表达式 math_errhandling & MATH_ERRNO 为零,则 errno 应设置为对应于该错误的值或保持不变。

but earlier on, when specifying errno.h, the standard says this: 7.5p3: […] The value of errno may be set to nonzero by a library function call whether or not there is an error, provided the use of errno is not documented in the description of the function in this document. errno is documented in the description for these functions, and that description makes no mention of setting errno when math_errhandling & MATH_ERRNO is zero. so that suggests that posix’s behavior is non-conformant.

但在更早之前,在规定 errno.h 时,标准这样写道:7.5p3:[…] 如果库函数调用在本文档的函数描述中未记录 errno 的使用,则无论是否发生错误,该函数调用都可能将 errno 的值设置为非零。这些函数的描述中记录了 errno,而该描述并未提及当 math_errhandling & MATH_ERRNO 为零时设置 errno 的情况。因此,这表明 POSIX 的行为是不符合标准的。

but hang on! everything i’ve talked about so far is only for overflow and underflow. but if the string is malformed and can’t be parsed as a number, then the standard doesn’t specify any error at all: 7.25.2.6p11: The functions return the converted value, if any. If no conversion could be performed, positive or unsigned zero is returned.

等等!到目前为止我所谈论的一切仅针对溢出和下溢。但如果字符串格式错误且无法解析为数字,那么标准根本没有指定任何错误:7.25.2.6p11:函数返回转换后的值(如果有)。如果无法执行转换,则返回正零或无符号零。

instead, you’re supposed to use the endptr parameter, and check afterward if endptr == nptr (i.e. the end pointer is the same as the start pointer, so no data was parsed): 7.25.2.6p8: If the subject sequence is empty or does not have the expected form, no conversion is performed; the value of nptr is stored in the object pointed to by endptr, provided that endptr is not a null pointer.

相反,你应该使用 endptr 参数,并在之后检查 endptr == nptr(即结束指针与起始指针相同,因此没有解析任何数据):7.25.2.6p8:如果主体序列为空或不具有预期形式,则不执行转换;如果 endptr 不是空指针,则 nptr 的值存储在 endptr 指向的对象中。

the man page strtod(3) says something similar: If no conversion is performed, zero is returned and (unless endptr is null) the value of nptr is stored in the location referenced by endptr. but check out what posix says: Upon successful completion, these functions shall return the converted value. If no conversion could be performed, 0 shall be returned, and errno may be set to [EINVAL].

strtod(3) 手册页也说了类似的话:如果未执行转换,则返回零,并且(除非 endptr 为空)nptr 的值存储在 endptr 引用的位置。但看看 POSIX 是怎么说的:成功完成后,这些函数应返回转换后的值。如果无法执行转换,则应返回 0,并且 errno 可能被设置为 [EINVAL]。

the word “may” basically means that it’s implementation-defined. but this is a big deal, because it’s pretty common to check for errors by doing something like this: errno = 0; double x = strtod(s, nullptr); if (errno != 0) { // … } strtod(3) suggests doing exactly this: Since 0 can be legitimately returned on both success and failure,

“可能”一词基本上意味着它是实现定义的。但这很重要,因为通过类似这样的方式检查错误非常普遍:errno = 0; double x = strtod(s, nullptr); if (errno != 0) { // ... } strtod(3) 建议这样做:由于 0 在成功和失败时都可能被合法返回,