NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
▲No portable way to detect string-to-float conversion errors in standard C (sebsite.pw)
Joker_vD 4 hours ago [-]
It's quite astonishing how the historical inability of commonly used programming languages (from FORTRAN all the way to, say, Java/C#) to ergonomically return two values from a function call has caused people to invent all kinds of insane APIs/contracts.

    [double x, errno_t errno] = strtod(s, nullptr);
    if (errno != 0) {
        // ...
    }

    [x, _] = strtod(s, nullptr);  // When you're certain the error is impossible or just don't care
Instead, we're stuck with "check if the return value is -1 and if it is, check the global variable errno" at best, and with "the function doesn't really reliably reports its errors so don't feed it malformed data, idc" at worst.
gucci-on-fleek 2 hours ago [-]
C has supported struct returns since at least C89:

  /* Compile with "gcc -std=c89 -pedantic -Wall -Wextra -Werror". */
  #include <errno.h>
  #include <stdio.h>
  #include <stdlib.h>
  
  typedef struct {
      double value;
      enum { NO_ERROR = 0, PARSE_ERROR, TOO_BIG } error;
  } result_t;
  
  static result_t parse_double(char const *str) {
      char *endptr;
      double parsed;
      result_t result;
  
      errno  = 0;
      parsed = strtod(str, &endptr);
  
      if (endptr == str) {
          result.value = 0.0;
          result.error = PARSE_ERROR;
      } else if (errno == ERANGE) {
          result.value = 0.0;
          result.error = TOO_BIG;
      } else {
          result.value = parsed;
          result.error = NO_ERROR;
      }
      return result;
  }
  
  int main(void) {
      result_t r;
  
      r = parse_double("1.23");
      printf("value: %f, error: %d\n", r.value, r.error);
  
      r = parse_double("abc");
      printf("value: %f, error: %d\n", r.value, r.error);
  
      r = parse_double("1e5000");
      printf("value: %f, error: %d\n", r.value, r.error);
  
      return 0;
  }
Needing to typedef a unique struct for every possible function return type isn't very ergonomic, but you can work around that with C23:

  // Compile with "gcc -std=c23 -pedantic -Wall -Wextra -Werror".
  #include <errno.h>
  #include <stdio.h>
  #include <stdlib.h>
  
  typedef enum {
      NO_ERROR = false,
      PARSE_ERROR,
      TOO_BIG,
  } errors;
  
  static struct { double value; errors error; } parse_double(char const *str) {
      errno         = 0;
      char *endptr  = nullptr;
      double parsed = strtod(str, &endptr);
  
      typeof(parse_double(str)) result;
      if (endptr == str) {
          result.value = 0.0;
          result.error = PARSE_ERROR;
      } else if (errno == ERANGE) {
          result.value = 0.0;
          result.error = TOO_BIG;
      } else {
          result.value = parsed;
          result.error = NO_ERROR;
      }
      return result;
  }
  
  int main() {
      auto r = parse_double("1.23");
      printf("value: %f, error: %d\n", r.value, r.error);
  
      r = parse_double("abc");
      printf("value: %f, error: %d\n", r.value, r.error);
  
      r = parse_double("1e5000");
      printf("value: %f, error: %d\n", r.value, r.error);
  
      return 0;
  }
wongarsu 1 hours ago [-]
Java and C# famously solve this with a hidden implicit return value for errors: the exception. The current trend in language design is to take the same idea and make it explicit instead, using sum types. Like Rust's `Result`, where the result contains either the normal value or an error, but never both

Of course C++ gives you the option to do any of the above, and still contains insane API contracts

qznc 59 minutes ago [-]
I would not describe exceptions as return values. Speaking in a C context, exceptions are more like "longjmp" than "return".
cubefox 29 minutes ago [-]
Arguably the better solution compared to these "algebraic sum types" (they go by countless different names) are set-theoretic union types, used by a few languages such as TypeScript.

They allow for types like "String or Integer". A function which returns a value of type "A" can automatically also be assigned to a variable of type "A or B", and a function which accepts a parameter of type "A or B" can automatically aldo be passed a variable of type "A". Which is not the case for algebraic sum types, which require somewhat inconvenient wrapping or unwrapping.

Unlike algebraic sum types, set-theoretic union types are also not necessarily mutually exclusive. E.g. a number of type "Integer or Float" could be both. Moreover, set-theoretic types can be generalized to arbitrary Boolean combinations of types, e.g. "A or (B and not C)", possibly involving disjunctions, conjunctions, and negations (since {and, or, not} are functionally complete). That's not possible for algebraic sum/product data types. Though there is no mainstream language yet with a set-theoretic type system that supports negation types.

kaashif 3 hours ago [-]
C++/C#/Python absolutely do have ergonomic ways to do this (and have for decades by this point...) but programmers continue to use sentinel values. It is a bit puzzling.
sdcfgy 53 minutes ago [-]
When I write C I tend to always return any error code on the stack and pass the pointer to where I want the result to go. Makes managing memory easier. The owner gets to allocate.

Example:

  #define E_DIVZERO -100
  int divide(float a, float b, float *result) { 
      if (b == 0.0) return E_DIVZERO;
      *result = a / b;
  }
tester756 2 hours ago [-]
Wut? C# besides all other ways to return Tuples, Result<T>s, NULLable, etc, etc.

Still had something as sane as:

if (double.TryParse("5.x5", out var val)) { Console.WriteLine(val); } else { Console.WriteLine("err"); }

edwcross 3 hours ago [-]
errno is global, but also _Thread_local, to help mitigate issues in a multithreaded environment. A kludge over a kludge...
muvlon 2 hours ago [-]
Heck, even using multiple returns for this feels like a kludge to me at this point, when more advanced languages like TypeScript, Rust or Zig offer sum types/tagged unions.
dwattttt 2 hours ago [-]
I'd argue it's as much a mentality as language barrier; high assurance code would typically always return an error code, and use out parameters rather than return values. This has been possible for as long as C has existed (I imagine), it's just not common practice.

I have a pet theory that new languages benefit at least as much from an opportunity to reset "common practice" / defaults as they do syntax/semantic improvements.

jdthedisciple 1 hours ago [-]
Tbf from what I coarsely remember having > 1 return value from a function was taught as bad practice and to be avoided in my days...
psychoslave 1 hours ago [-]
Doesn't even need two return values. One tagged union suffices:

  Result(double) x = strtod·or·error(s);
  
  if (!x.ok) {
      handle(x.error);
      return;
  }
  
  use(x.value);

Which in real C, with no language support, is just:

  typedef struct {
      double value;
      int    error;   /* 0 = ok */
  } maybe·double;
  
  maybe·double x = strtod·or·error(s, NULL);
  if (x.error) { /* handle */ }
fragmede 4 hours ago [-]
Thank you for reminding me that, while Python is slow, there are still reasons to pick it over the alternatives.
nvme0n1p1 3 hours ago [-]
Basically all commonly used languages support this nowadays. Even ones you might not expect like C++ and C# have added it. The only major exceptions that come to mind are C and Java.
RossBencina 2 hours ago [-]
You have been able to return a struct by value in C since at least C89:

    typedef struct {
        double d;
        int err;
    } Ret;

    Ret foo(void) {
        Ret r = { 0.0, 43};
        return r;
    }

    int main() {
        Ret r = foo();
    }
You can't return a bare fixed-size array though.
Panzerschrek 4 hours ago [-]
C number parsing functions are fundamentally broken, since they are locale-dependend. There are many bugs when one parses a number assuming it has decimal point, but later on systems with locale different from one used on developer's machine it may be suddenly comma instead of point. So, one should avoid using them almost always and use std::from_chars (available in new C++ versions) or some other thirdparty library.
spacechild1 4 hours ago [-]
+1 for `std::from_chars`. It's also by far the fastest of all standard conversion routines.
raverbashing 3 hours ago [-]
And errno

errno is a worse mistake than null pointers in general

gucci-on-fleek 2 hours ago [-]
> the linux man pages for these functions literally don't mention this at all

> the fact that the man page never makes any note of this being an extension ([…]) is really notable to me.

> this is completely undocumented on the linux man page.

The Linux man page maintainers are really friendly in my experience, so it's probably worth reporting this to them [0] [1].

[0]: https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/...

[1]: https://lore.kernel.org/linux-man/

RossBencina 2 hours ago [-]
For a long time Microsoft's strtod implementation was broken, I'd think most people would be using netlib or one of the newer algorithms.

https://www.exploringbinary.com/visual-c-plus-plus-strtod-st...

https://lemire.me/blog/2026/02/01/converting-floats-to-strin...

chasil 5 hours ago [-]
IEEE-754 is a harsh mistress.

https://en.wikipedia.org/wiki/IEEE_754

chasil 5 hours ago [-]
The VAX floating point was advanced as an alternative standards proposal at that time. Intel won the argument.

https://sixlettervariable.blogspot.com/2007/05/vax-floating-...

adrian_b 2 hours ago [-]
For good reasons. The VAX floating point specification was much worse, even if somewhat cheaper to implement.

IEEE 754 was one of the few standards that caused a significant progress of the standardized domain, because what is important for users had precedence over what is important for vendors.

Before IEEE 754, all floating-point implementations attempted to minimize the hardware cost, regardless of what aberrant behavior this would cause, which would confuse the naive users.

anitil 5 hours ago [-]
Even putting aside the ugliness of errno, this is pretty bad. I'm at the point with libc where I try to use as little of it as I can, though I'm not writing a lot of C these days
signa11 4 hours ago [-]
well, unless you are 'syscalling' someone else is doing it on your behalf.
eptcyka 4 hours ago [-]
More to libc than syscalls.
PhilipRoman 3 hours ago [-]
I would say there are 3 broad classes of libc functions

1) functions that you cannot implement in portable C - syscalls, signals, ABI specific things like longjmp and setcontext

2) functions that you could implement by yourself in portable C, but they are expected to play nice with the rest of the system - a lot of POSIX user stuff, dns management, dlopen, all the nsswitch mess. Can't think of any standard C examples because of the extreme portability requirements of C. Maybe the locale stuff, although it's usefulness is dubious.

3) functions that are there for convenience and sometimes compiler special-casing, like math, str*, <db.h> stuff that was thankfully removed from glibc

Most of #3 should be ignored. #2 is optional, especially if your software is for end users and not packaged by a distro. Sometimes it's safer to ship your own MDNS implementation for example rather than hope that the distro has sane defaults.

eptcyka 2 hours ago [-]
I'd much rather push for a world where we use a reasonable DNS/mDNS implementation on Linux rather than add to the zoo of bespoke variants. I have written code that tries to appease different DNS managers across different versions of them for different distros. We need to stop. Everyone believes that the common way of handling DNS is insane, so they create their own new way of doing it.
signa11 3 hours ago [-]
ofcourse. forgot the tongue-firmly-in-the-cheek disclaimer.
oso2k 2 hours ago [-]
Sometimes I wish we could get to a timeline where the FPU format standards have something like Crockford’s DEC64 end up winning or evolving into the prevailing standard. Simpler & consistent math ought not to be this hard.

https://www.crockford.com/dec64.html

j16sdiz 5 hours ago [-]
yes. that's why many number crunching app ships / vendors their own math library
CamperBob2 5 hours ago [-]

     if (sscanf(string,"%lf",&foo) != 1) 
        { 
        bad_float(); 
        }
This has the benefit of handling scientific notation, too, which some of the other CRTL functions inexplicably do not.

For extra reassurance, initialize foo to NaN or DBL_MAX or something, and verify that it is no longer equal to that value.

lelanthran 4 hours ago [-]
That doesn't detect overflows and underflows. Also changes based on locale, which is not thread safe (so you cannot set locale, convert, then unset locale).
geocar 2 hours ago [-]
> That doesn't detect overflows and underflows

A trivial change does though: Just round-trip it.

    int s2d(double*restrict out,const char*restrict in){
     char tmp[99];int r,q=sscanf(in,"%lf%n",out,&r),s=sprintf(tmp,"%lf.",*out);
     return q&&!((r>s)||memcmp(in,tmp,r)||tmp[r]>'0');
    }
> Also changes based on locale,

That's a feature: Where did the string come from if not the user?

> you cannot set locale, convert, then unset locale

So what? You're not handling multiple locales: You're using the user's locale. If you're making a server, using anything but LANG=C or a utf8 variant is asking for trouble.

I think the point is it's absolutely possible to portably detect errors, some libc's strtod() just don't which ain't the same thing.

daemin 1 hours ago [-]
>> Also changes based on locale,

> That's a feature: Where did the string come from if not the user?

Many games and applications store floating point values in text file formats. This has caused bugs in those programs many times over because they didn't realise it was locale dependent and the data parsing broke in another locale.

shakna 4 hours ago [-]
sscanf has locale things that make it include implementation-defined behaviour, doesn't it?
tliltocatl 3 hours ago [-]
Anything that isn't C.UTF-8 is totally broken anyway.
PhilipRoman 3 hours ago [-]
Case in point: echo y | LANG=lv_LV.UTF-8 grep '[a-z]'
TZubiri 4 hours ago [-]
" Dinesh: I said I would do DRM you would do error handling.

Gilfoyle: Anything to do with errors is more your vibe"

https://www.youtube.com/watch?v=oyVksFviJVE&t=49

Oh, this one is easy.

Just don't make the errors, then you don't have to detect them.

When your app stops issuing these errors, then you won't have to write blog posts complaining that someone elses code can't handle your errors.

Definitely you should never be converting strings to floats ever anyways, see how I'm going on a limb here without any knowledge of your application, but then you know that I'm right because there's no reason why you would ever do that?

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 09:36:48 GMT+0000 (Coordinated Universal Time) with Vercel.