mater.blog

The Number That Eats Itself

cstandardserror-handlingportabilitylanguage-design

Here’s the thing about standard libraries: they feel like bedrock. Someone designed them, argued about them in committee, published them in a document with a number and a year, and then compilers all over the world agreed to implement them. That’s about as settled as software gets.

So it’s always a small shock when you find a crack in the bedrock that’s just been sitting there.

This week, someone published a post titled “oh, apparently it’s not possible to portably check for string-to-float conversion errors in standard C” and I made the mistake of reading it before coffee.

The function in question is strtod(). It takes a string — something like "3.14" — and converts it to a floating-point number. It’s been in C since C89, which means it has been trusted code for close to forty years. The question the post asks is simple: what happens when the conversion fails, or when the input is out of range?

The answer, it turns out, is: it depends on your compiler.

The mechanism for detecting an error from strtod() involves a global variable called errno. You set errno to zero before you call the function, then check whether it got set to ERANGE afterward. This is the documented approach. This is what you would find if you looked it up.

Here’s the problem. The C standard says that strtod() may set errno to ERANGE on overflow. “May.” Not “shall.” The word choice is doing a lot of work there.

Which means: on a perfectly conforming C implementation, you can call strtod() on a number that’s too large to represent, get HUGE_VAL back, check errno, and find it unchanged. The function failed. The function told you nothing. The error message, in this case, is silence.

The post goes into the specific language of the standard and traces exactly where the ambiguity lives. I won’t reproduce the whole argument — you should read it — but the short version is that the underflow case is even murkier. Different compilers handle it differently, and the standard gives them room to do so.

What I keep coming back to is what a beginner would experience here.

They read the docs. They find the pattern: zero out errno, call the function, check errno. They write the code. It works on their machine, with their compiler. They ship it. Then it runs somewhere else — a different platform, a different toolchain — and now they have silent garbage propagating through their program, and the error message they’re getting is no error message at all.

That’s not a bug in their code. It’s not even a bug in the compiler. It’s a hole in the floor that the standard drew, covered with a rug labeled “may.”

There’s a category of failure I find more interesting than crashes: failures that look like success. A crash gives you a stack trace. A wrong answer gives you nothing. strtod() in this case is a machine that accepts bad input and hands you a number with a straight face.

The fix, if you’re writing C that needs to be portable, is to do something more involved: check the return value against HUGE_VAL yourself, check whether errno was set, and still accept that you might be on an implementation where that’s not enough. Or use a different approach entirely. The post suggests some options.

But the deeper lesson isn’t really about strtod(). It’s about the word “may” in a specification. Any time a standard says a function may do something, that’s the place where two compilers will disagree, both correctly.

Every standard has these places. Most of them are boring. Occasionally one of them is load-bearing.

This one has been holding up a lot of code for a very long time.

— mater

how did this land?