NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
▲No portable way to detect string-to-float conversion errors in standard C (sebsite.pw)
Panzerschrek 2 days 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.
account42 13 hours ago [-]
Locale-dependence should have always been explicit with a required locale parameter. Unfortunately we're not only stuck with this legacy global locale mess but even many new additions to C and C++ repeat this mistake.
spacechild1 2 days ago [-]
+1 for `std::from_chars`. It's also by far the fastest of all standard conversion routines.
raverbashing 2 days ago [-]
And errno

errno is a worse mistake than null pointers in general

masklinn 16 hours ago [-]
Well no, because everybody else looked at errno and thought “wow that’s fucking stupid”, so it’s exclusively a self-inflicted libc problem not a near universal plague.
account42 13 hours ago [-]
GetLastError-like functions are unfortunately all too common in C APIs.
Joker_vD 2 days 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 days 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;
  }
fps-hero 1 days ago [-]
> Needing to typedef a unique struct for every possible function return type isn't very ergonomic, but you can work around that with C23:

I think you’ve unraveled the mystery of why that didn’t catch on in nearly 40 years of C.

Honestly it’s got to be the biggest disappointment in computer science that nothing has displaced C as the Franca Lingua. And now with the advent of AI, I don’t think anyone is going to care anymore. Rust will be the future, but not because it’s ergonomic, but because it can prevent memory errors.

wongarsu 2 days 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

TeMPOraL 1 days ago [-]
That doesn't solve the whole problem, though - as per the rule, "zero, one, infinity", you often want to return more than two values, and often none of them represents an error.
tialaramex 24 hours ago [-]
But Rust doesn't care how many things you want to return?

    fn six_numbers() -> (u8, u8, u8, u8, u8, u8) {
        (10, 20, 30, 40, 50, 60)
    }

    fn in_an_array() -> [u8; 6] {
        [10, 20, 30, 40, 50, 60]
    }
The return type must be sized, because otherwise how can we put that anywhere? But of course your answer can just be to put it on the heap inside a Box...

    fn heap_numbers() -> Box<[u8]> {
        let mut slice = Box::new_uninit_slice(6);
        slice[0].write(10);
        slice[1].write(20);
        slice[2].write(30);
        slice[3].write(40);
        slice[4].write(50);
        slice[5].write(60);
        unsafe { slice.assume_init() }
    }
zelcon 17 hours ago [-]
C++ can do this as well with tuple unpacking. Rust copied it from C++.
cyphar 11 hours ago [-]
I'm not a C++ guy (aside from the odd patch to upstream projects) so maybe I'm missing something but are you talking about structured bindings? AFAICS those were added in C++17, so Rust had them for much longer. Besides, I would argue Python is the real inspiration for C++ here.

In Rust "tuple unpacking" is really just a special case of pattern matching, which is a far more powerful concept and was actually taken from OCaml.

tialaramex 14 hours ago [-]
It seems more likely that Rust, an expression language with pattern matching, has the thing which makes sense here because this is just a pattern match.

From Rust's point of view

    let (a, b) = function_which_returns_a_pair();
is not special compared to the idiomatic:

    while let Some(item) = stack.pop() { /* do stuff with items and the stack */ }
or if we hate tuples:

    let [a, b] = function_returns_an_array_of_two_items();
12 hours ago [-]
qznc 2 days ago [-]
I would not describe exceptions as return values. Speaking in a C context, exceptions are more like "longjmp" than "return".
jonenst 2 days ago [-]
They're both ! Exception are definitely viewed as an alternate communication channel. And stack unwinding is one aspect of the communication. See https://nedbatchelder.com/text/exceptions-vs-status#h_valuab... (and many other)
doctorpangloss 23 hours ago [-]
Was it ever about the language? Or was it really about, what kind of special interests can be tickled in the authors of greenfield, free software?

Programming language development has essentially ceased, no? Because now that you have a super powerful, ceaseless special interest personality that can be ordered to be specially interested in any problem. The actual problem - a dearth of things that special interest personalities will do for little to no money - has been solved.

ameliaquining 8 hours ago [-]
Programming languages take decades to get traction. We've had LLMs capable of real software engineering for less than two years. It's way too early to make pronouncements about this.
doctorpangloss 3 hours ago [-]
i don't know, apple says, you must use XYZ to develop for the iphone, and overnight, people will use it. most people correctly view this stuff as a "parade of tedious obstacles."

it's not really about traction or adoption so much as it is, the average developer values an ecosystem of free shit to use to make money.

the slow part is, the decades-long pipeline that converts the finite number of specially interested people created every year into programmers who sit around and do stuff for giant corporations, practically speaking, for free. maybe that is way too cynical but am i wrong? LLMs have had a range of programming performances, but for the whole time they have existed, they have not had difficult personalities. which is far, far more valuable. the majority of people in the white collar workforce are there because they are, #1, pleasant to work with, not good at their jobs. and look, specially interested people, who will pay huge amounts of money in order to avoid contact with people, agree!

cubefox 2 days 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 also be passed a variable of type "A". This 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.

tialaramex 2 days ago [-]
This choice has nice ergonomics but brutal practical cost so that's why languages close to the metal do not want to do this and particularly don't want to go around defaulting to it whereas a language like Typescript is fine with it.
cubefox 1 days ago [-]
Maybe it has "brutal practical cost", I don't know, but even most high-level languages don't have set-theoretic type systems. Perhaps because the approach is relatively new and most popular languages are old.
ablob 7 minutes ago [-]
the set theoretic type system variant has one issue I think the algebraic one avoids: An overlap between types is ambiguous, i.e. "int | int" doesn't make any sense to write. In a function parse_postal_code, where you either get the postal code, or an error code you wouldn't be able to differentiate. You could write your own types for that, but now you've essentially recreated what the algebraic versions do by forcing a selection through the type constructor on the inner type instead of the wrapping union type.

That being said, in cases where there is no overlap I don't see why "the old way" shouldn't behave like typescript through implicit conversion as there is only one applicable constructor. Forcing explicit selection if there is overlap, but do it automatically when there is none seems like reasonable behavior that is a strict superset of what you wrote before.

sparkie 1 days ago [-]
Sum types in the set theoretic approach can be made with `(A & ¬B) | (¬A & B)`, or equivalently `(A | B) & (¬A | ¬B)`.

There's no mainstream language with negation types, but a few good research languages with them, eg MLstruct.

[MLstruct]:https://github.com/hkust-taco/mlstruct

cubefox 1 days ago [-]
> MLstruct supports union, intersection, and complement (or negation) connectives. We make sure they form a Boolean algebra and add enough structure to derive a sound and complete type inference algorithm. For more details, please see the paper mentioned above.

This is cool. Unfortunately it's also too late now. The machines have taken over programming.

sdcfgy 2 days 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;
  }
sparkie 1 days ago [-]
If you do:

    typedef struct {
        int status;
        float value;
    } div_result_t;

    div_result_t divide(float a, float b) {
        if (b = 0.0) return (div_result_t){ E_DIVZERO };
        return (div_result_t){ 0, a / b };
    }
On SYSV: The `status` gets returned in x0/rax, and `value` gets returned in x1/xmm0.

On MSVC: The caller allocates `sizeof(div_result_t)` on the stack, passes the the pointer to that space as an implicit first argument to `divide` in register x0/rcx. The function returns the same pointer as its result in x0/rax.

The latter is almost equivalent to you passing a pointer manually - but the compiler does it for you. In the former case, it's actually more efficient to use the struct as we don't even need to touch the stack - all arguments and returns are in registers (for structs up to two "eightbytes" - ie, 16-bytes).

IMO, if it intended to use automatic storage duration, and the size of the struct is small enough, returning by value is preferable - it performs better on SYSV, and basically automates what you would do manually on MSVC. If it is intended for manual storage duration, then yes, use pass and return by pointer explicitly.

quietbritishjim 2 days ago [-]
I agree, but you're missing the `return E_SUCCESS` at the end :-)
sdcfgy 1 days ago [-]
Good catch. That was pre-coffee :)
layer8 1 days ago [-]
In C you can return a struct with two members. The reason the standard library doesn’t do this isn’t ergonomics, it’s performance, or maybe the traditional inability of compilers to map it to what would be the idiomatic register assignment, due to how ABIs are defined.
kaashif 2 days 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.
saalweachter 23 hours ago [-]
You've got two huge filters on C++ codebase quality.

First, you have the "our core libraries/dependencies were written when it was 'C with Classes' and we're going to keep using that style for consistency/laziness/because it's well-tested", and then you have the "C++ is ugly/full of segfaults/hard, I'm going to use <new language> which solves all these problems".

You have to pass those two filters before you get to people who are interested in writing good, modern C++.

pjmlp 17 hours ago [-]
Indeed, while undoubtedly we need "C++ The Good Parts", since C++17, but C++20 would be better cut, we can perfectly write Python like code in C++, while getting the performance Python will never deliver.
tester756 2 days 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"); }

Joker_vD 1 days ago [-]
C# got Tuple only in version 7.0 which was released in 2017, if you don't know; while TryParse has been there since 1.0 in 2002. And I'd still prefer to have e.g.

    if ([double val, bool ok] = double.TryParse("5.x5"); ok) {
        Console.WriteLine(val);
    } else {
        Console.WriteLine("err");
    }
Of course, having "var" there would be even nicer.
ablob 57 seconds ago [-]
I don't really see the semantic difference between your code snippet and

  if (double.TryParse("5.x5", out double val)) { ...

which would be the C# equivalent
tester756 3 hours ago [-]
"only" 2017? :P it's almost 10 years ago

Anyway C# has this:

var (_, _, population1960, _, population2010) = QueryPopulationDataForYears("New York City", 1960, 2010);

kfuse 1 days ago [-]
This golangism hurts readability, it puts an important piece of logic at the end of a pontentially long line. Usually, it's just an ok test, but it's easy to miss when it isn't.
Joker_vD 19 hours ago [-]
> This golangism hurts readability,

Lolwut? It's basically the same syntax that "for" has, which is why C2Y is finally getting it; besides, C++ already got it in 2017 — or even in 1998, if you count the "if (int x = f()) { ... }" shape.

kfuse 19 minutes ago [-]
"For" statement can become quite messy if used outside of several common patterns. And the fact that c/c++ are getting this so late shows that this is not such an obviously good idea.
muvlon 2 days 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.
cubefox 2 days ago [-]
Strictly speaking the unions in TS are untagged/set-theoretic, which is different from Rust or Zig.
win311fwg 1 days ago [-]
Except often you want disjoined return arguments, while sum types setup an either-or situation. You can resort to tuples/arrays, but then that questions why the languages support multiple input arguments when tuples can be equally used for input. Frankly, any language that accepts multiple input arguments but only allows one return argument didn't have much thought put into it.
cowlevel 12 hours ago [-]
Or, equivalently, ergonomic product and sum types. Go has multiple returns and it's better than C but it's still annoying because it's the only thing you can return - Haskell lets you return a tuple, an Either, a Maybe, or so on.

In Go, the only way to consume a multi-return is to use a full assignment statement to assign each return value to a variable, and then use the variables. You can't put more than one multi-returning function in the same line of code because each one needs its own multi-assignment statement.

cyphar 11 hours ago [-]
> In Go, the only way to consume a multi-return is to use a full assignment statement to assign each return value to a variable, and then use the variables.

There actually is another way, but it isn't really that handy (as it is basically just shorthand for multiple assignment):

    func mustInt(v int, err error) int {
        fmt.Printf("got: (%v, %v)\n", v, err)
        return v
    }
    
    func foobar() (int, error) {
        return -123, nil
    }
    
    func badbeef() (int, error) {
        return nil, fmt.Errorf("moo")
    }
    
    func main() {
        vals := []any{
            mustFn(foobar()),
            mustFn(badbeef()),
        }
        fmt.Printf("got vals: %v\n", vals)
    }

You can use generics but it's still not that useful because you are quite limited in what you can do with the return value:

    func mustFn[T any](v T, err error) T {
        fmt.Printf("got: (%v, %v)\n", v, err)
        return v
    }
    
    func foobar() (int64, error) {
        return -123, nil
    }
    
    func deadbeef() (string, error) {
        return "weird name for a cafe", nil
    }
    
    func badbeef() (*int, error) {
        return nil, fmt.Errorf("moo")
    }
    
    func main() {
        vals := []any{
            mustFn(foobar()),
            mustFn(deadbeef()),
            mustFn(badbeef()),
        }
        fmt.Printf("got vals: %v\n", vals)
    }
You could define your own Result[T any] and do sum types (and people do) but the ergnomics for it are completely awful and readability goes out the window. (Insert grumbling about bolt-on features.)
11 hours ago [-]
Kevin_Flynn 1 days ago [-]
Is returning 2 values from a function ever required ?

If your code is organized well, and your functions act on context structures, a pass/fail result, or an error code where 0 = success, and everything else is an error, or a pointer to a structure with multiple values, seems to be entirely sufficient.

I quite prefer C, over C++ and other perhaps "more expressive" languages, exactly because it is simpler. It does more with less.

"It can scarcely be denied that the supreme goal of all theory is to make the irreducible basic elements as simple and as few as possible without having to surrender the adequate representation of a single datum of experience." - Albert Einstein

lpribis 1 days ago [-]
> returning 2 values

> act on context structures

Same semantics but mutating context is much harder to track, so yes it is preferred to return 2 values.

Kevin_Flynn 1 days ago [-]
So, returning 2 values is more performant than returning 1.

How does this affect branch prediction ?

When code is structued:

  ...

  result = function1(arg1,arg2,etc);
  if ( !result ) handle_error(result);

  result = function2(arg1,arg2,etc);
  if ( !result ) handle_error(result);

  ...
branch prediction performs well.

I guess if you're checking and branching on 2 values, it performs twice as well ?

sparkie 1 days ago [-]
Returning 2 values can be more performant than returning one and assigning to another as an "out parameter".

Eg:

    typedef struct {
        bool has_value;
        int value;
    } option_int_t;

    option_int_t foo() {
        if (somecond) return (option_int_t){ true, somevalue };
        else return (option_int_t){ false };
    }
  
    bool bar(int *out) {
        if (somecond) {
            *out = somevalue;
            return true;
        }
        return false;
    }
`foo` is slightly more efficient than `bar` on 64-bit SYSV, whose ABI supports 128-bit ("two eightbytes") returns in hardware registers. `bar` requires at least one dereference to assign the result.

On other compilers this isn't the case. MSVC only supports 64-bit returns and would use the stack for `option_int_t`. The caller allocates the space and an implicit pointer is passed in the first argument register (eg, `rcx`), and the same pointer is returned as the result.

dwattttt 2 days 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.

edwcross 2 days ago [-]
errno is global, but also _Thread_local, to help mitigate issues in a multithreaded environment. A kludge over a kludge...
anitil 24 hours ago [-]
I was about to say that - it's really a mess
StilesCrisis 1 days ago [-]
C allows struct returns, and C++ allows something quite close to what you've written verbatim. But in 1989, there was less of a focus on error handling; most code wasn't checking errno at all, so why spend time returning it to every caller?
jdthedisciple 2 days 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 2 days 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 */ }
win311fwg 1 days ago [-]
Why no tagged unions in the C version?

   typedef struct {
        _Bool ok;
        union {
            double value;
            int error;
        };
    } Result_double;

    Result_double x = strtod·or·error(s, NULL);

    if (!x.ok) {
        handle(x.error);
        return;
    }

    use(x.value);
sparkie 1 days ago [-]

  typedef struct {
      double value;
      int    error;
  } maybe_double;
Is most efficient on x86-64 SYSV, because the ABI supports the `error` being returned in a GP register, and the `value` in a vector register. The struct is returned in `rax:xmm0` - the double is where it is meant to be.

When you use a union containing an int and a double, the double has to be moved to a GP register, and back to the vector register when read as a double (via movq), so there's a small hidden cost compared to the former approach. Your `Result_double` is returned in `rax:rdx`.

There's a trick you can use to avoid this - keep the `double` in the vector register and move the `int` to the vector register instead - so you only pay the cost to move between GP and vector registers when there's an error but not otherwise. To do this, make the union use `__m64` instead of `int` - the whole union is then in a vector register, and the `Result_double` struct is returned in `rax:xmm0`, as with `maybe_double` struct.

   typedef struct {
        _Bool ok;
        union {
            double value;
            __m64 error;
        };
    } Result_double;
Of course, this is x86-64 SYSV only. On AARCH64, the double will always be moved to a GP register in either example. Other conventions may also differ - MSVC in particular is poor because it converts to pass-by-pointer to a stack allocated structure for any struct greater than 64-bits.

On MSVC we could maybe get maximum efficiency by using NaN-boxing - store the error code in the signalling NaN space - eg, treat NaNs beginning 0x7FF1 as error codes.

   typedef union {
       int64_t _as_int;
       double _as_double;
   } Result_double;
   
   #define Result_double_value(n) \
       (Result_double){ ._as_double = (n) };
   #define Result_double_error(errcode) \ 
       (Result_double){ ._as_int = (0x7FF1000000000000ULL | (errcode)) }
   #define Result_double_is_error(x) \
       (((x)._as_int & 0x7FFF000000000000ULL) == 0x7FF1000000000000ULL)
   #define Result_double_get_error(x) \
       ((x)._as_int & 0x0000FFFFFFFFFFFFULL)
   #define Result_double_get_value(x) \
       ((x)._as_double)

   Result_double x = strtod·or·error(s, NULL)
   if (Result_double_is_error(x)) {
      handle(Result_double_get_error(x));
      return;
   }
   use(Result_double_get_value(x))
The `Result_double` then gets returned in `rax` and avoids touching the stack. Could probably improve this to return in `xmm0` instead.
fragmede 2 days ago [-]
Thank you for reminding me that, while Python is slow, there are still reasons to pick it over the alternatives.
nvme0n1p1 2 days 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 days 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.
pjmlp 17 hours ago [-]
You don't need special syntax to return a struct like type, so naturally it is also available in C and Java.
nvme0n1p1 17 hours ago [-]
GP said "ergonomically return two values from a function call" and shows an example of destructuring.

  foo, bar = compute_foo_and_bar()
If you don't care about ergonomics, then sure, C and Java are turing complete so there's always a way to get values where you need them, albeit with a few extra lines of code.

  FooAndBar foo_and_bar = compute_foo_and_bar();
  Foo foo = foo_and_bar.foo;
  Bar bar = foo_and_bar.bar;
pjmlp 16 hours ago [-]
Java has destructuring, folks should update their Java knowledge, here for the Rust loving folks,

        switch(computeFooAndBar()) {
            case Result(var foo, var bar) -> System.out.println("one case");

            default -> System.out.println("Something else");
        }
-- https://godbolt.org/z/369cfbEv1
nvme0n1p1 6 hours ago [-]
That's pattern matching, not destructuring. If you need to use 3 or more lines of code to simulate destructuring, it's better to just stick with the pattern I showed for C, so you don't have to introduce a spurious indent level for every assignment.
gucci-on-fleek 2 days 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/

jlokier 21 hours ago [-]
A subtlety around denormals too. In the article, it quotes a definition of ERANGE underflow as closer to zero than the smallest _normalised_ positive value.

Denormals therefore count as underflow and strtod() sets ERANGE for them, even though they they are legitimate values to store as strings and parse back.

Some implementations of strtod() return the correct floating-point value for these, while setting ERANGE. Others discard the value and return 0.0, preventing them from being round-tripped to strings.

thomashabets2 1 days ago [-]
Lerc 1 days ago [-]
When I was writing some ASM for a microcontroller to do string <--> float conversion, I could find almost nothing on how to best perform the operations.

There were essentially three ways to do it. Muddle through with a naive implementation. Look at the ASM of an old 8-bit machine. The final category was the by far the most dominant suggestion, to the point that anything else is almost like lost knowledge now. That solution was to use the C standard functions.

It seems like this is a sufficiently unpleasant task that as soon as anyone didn't absolutely have to do it all themselves, the punted to a black box solution.

There's probably something to be said there about the plethora of hobbiest 3d engines in comparison.

RossBencina 2 days 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...

kazinator 1 days ago [-]
Don't forget locale! If you're in the wrong locale strtod("3,4", ...) may return 3.4, etc.
anitil 2 days 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 2 days ago [-]
well, unless you are 'syscalling' someone else is doing it on your behalf.
eptcyka 2 days ago [-]
More to libc than syscalls.
PhilipRoman 2 days 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 days 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.
mrlonglong 1 days ago [-]
account42 13 hours ago [-]
How would you write dlopen in portable C? Once you are loading code and calling into that its no longer portable C and on modern platforms you'd need at least something like mmap/mprotect to allow you to do that, which are POSIX but not C.
PhilipRoman 9 hours ago [-]
Maybe that wasn't the best example since you cannot even cast to a function pointer within standard C (but you can in POSIX). If you consider POSIX as portable then writing your own dlopen is pretty simple and you don't even need to be aware of calling conventions, though there are some architecture specific things in the ELF format. But either way it will not integrate well with mechanisms that people expect to work like LD_* environment variables, /etc/ld.so.conf and so on. So I still think it matches the "possible but will not integrate with rest of the system"
cowlevel 12 hours ago [-]
I think it was meant to be read as functions that should play nicely with dlopen.

So you could write your own atexit, but maybe someone's got a version of atexit that cancels callbacks from unloaded libraries, or one that keeps them loaded if they have callbacks, or one that calls the callbacks immediately during dlclose.

signa11 2 days ago [-]
ofcourse. forgot the tongue-firmly-in-the-cheek disclaimer.
chasil 2 days ago [-]
IEEE-754 is a harsh mistress.

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

chasil 2 days 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 days 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.

j16sdiz 2 days ago [-]
yes. that's why many number crunching app ships / vendors their own math library
oso2k 2 days 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

TZubiri 2 days 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?

CamperBob2 2 days 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 2 days 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 days 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 2 days 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.

CamperBob2 1 days ago [-]
I don't support the nonstandard use of commas as decimal points in my software, so that's not an issue for me. That's a hill for the Europeans to live and die on. "Gotta be different from everybody else" has a price.
daemin 1 days ago [-]
Europeans* - you mean the rest of the world that isn't default English (Traditional or Simplified) speaking.
CamperBob2 1 days ago [-]
No, I do not. I mean a small but vocal minority of people who chose to change a longstanding historical convention that didn't need changing: https://chatgpt.com/c/6acd0706-1ec4-83e8-a538-980a20dc4fd1
daemin 1 days ago [-]
Since you're just regurgitating LLM output I'll just leave you with a link showing that the world is a rich and varied place: https://randombits.dev/articles/number-localization/locale-l...
CamperBob2 1 days ago [-]
Ah, yes, the Azerbaijanis and the Belorussians, can't forget them. About 27% of the world population in total, if the clanker's more detailed report is to be believed.

Regardless, it's goofy. Everybody should agree on one or the other, and it's too late for the other 73% to switch.

nz 13 hours ago [-]
Who hurt you?
CamperBob2 7 hours ago [-]
People who cause me to do unnecessary work when I'd rather be doing something else. People who make things more complicated than they need to be.

News flash: these people hurt you, too.

Life is finite, but unfortunately, bullshit like "Hey, let's use commas as decimal points, LOL" isn't.

lelanthran 1 days ago [-]
> "Gotta be different from everybody else" has a price.

Isn't it the US that is the "different from everyone else" in this context?

Just how shut-in are you?

geocar 2 days ago [-]
> Many games and applications store floating point values in text file formats.

And why, pray tell, would those games and applications ship with text files containing floating point values that were invalid?

Certainly if they produce the values, strtod() is sure to work, and worrying about errors at that level just doesn't make any sense at all.

> This has caused bugs in those programs

I'm sorry this just isn't true: The programmer caused the bugs in those programs and nobody else.

Meanwhile users who produce text files with floating point numbers in their locale expect program that load those text files to tolerate their locale, and those programs should not be using strtod()!

daemin 1 days ago [-]
The floating point values were valid, they were stored in the text files (unknowingly) in a locale dependent way, either by having a program use a locale sensitive functions to write the values, or because the values were written by hand using the developer's native way of number formatting. The values were then read using the locale dependent functions (like strtod) and if run on a machine with a locale that formatted numbers differently (i.e. using comma for decimal separator) they would return incorrect values.

This caused issues in the game as the values would be significantly different than originally designed, and it would be hard to test for since the people testing the game would usually be using a similar locale to the developer writing the game, and therefore they would only become apparent upon release or wider testing. Now the fix was to explicitly specify the locale before reading the data files, but it still took time to find and implement the fix.

geocar 1 days ago [-]
> The floating point values were valid

I would not agree. That sounds to me like the floating point values were not valid, because they were stored in the user's locale and were not intended for the user themselves to consume, but for the developer (who is not the user), or another person (who is also not the user, even if they are a user).

I understand maybe they "should be" valid, but they weren't, and the programmer should not have written files in the users locale that they did not expect the user to edit personally; that they intended to consume mechanically, even if they found it convenient as a human being to edit the file on occasion.

> differently (i.e. using comma for decimal separator) they would return incorrect values.

My suggestion reports an error. strtod() can also detect this error (that's what the second argument is for), so the programmer was not checking if they were using strtod(). This is also not the kind of error the article is talking about detecting.

daemin 1 days ago [-]
I don't think you understand the situation so I'll try explaining it one last time.

The developer stored floating point values in a text file written out by whatever means, in the developer's locale (say en-US). These files were shipped with the application and the values were read in using locale sensitive functions on a user's machine (the developer was unaware of the locale sensitive nature of these functions), and the user's locale was something different like de-DE for example. This resulted in a different value being read in compared to the value expected by the developer, therefore causing issues in the rest of the program.

For example the file stored "1.05" which in the developer's en-US locale is "one and five hundredths", but reading it on a different user's locale (de-DE) could result in the value being "105" (one hundred and five), or even "1" (one).

These files are generally not considered to be edited by users (unless they are modding the game, and then they do so without warranty etc).

geocar 9 hours ago [-]
> I don't think you understand the situation

I definitely do not.

> This resulted in a different value being read in compared to the value expected by the developer

> the developer was unaware of the locale sensitive nature of these functions

> causing issues in the rest of the program

Still sounds like "the programmer caused the bugs in those programs and nobody else" and they would not have that the problem you describe had they either used strtod() correctly or had used my parser.

Are you trying to tell me someone else put the bug in that programmers' code? Or do you actually think you have a pair of locales and a single floating point value that will produce two valid but different floats with my parser?

account42 13 hours ago [-]
> That's a feature: Where did the string come from if not the user?

The programs data files or files provided but not created by the user. This is not a theoretical issue but a real recurring source of bugs.

And not all programs have a singular user either. Global locale doesn't make any sense at all for server-like programs.

And then there are users that effectively have multiple locales because they need to deal with people, programs and websites written based on assumptions from other locales.

rwmj 2 days ago [-]
Annoyingly this doesn't detect certain errors. This is one thing I'd like to change in the next C standard, but (from discussions I had with someone who works in those standards) they don't want to break existing code. Which seems like a very bogus justification to me as that code is broken already.

Also the locale thing could be solved by adding another %-directive to say "this format string should be treated in the C locale", like %C (if it's not already used).

IOW this could be made to work, but human factors prevent it.

CamperBob2 1 days ago [-]
On my machine it detects overflows just fine, returning Inf and -Inf, and also correctly rounds underflows to 0 and -0. But the function itself still returns 1 for overflows/underflows, so it's up to the caller to filter out infinities.

I guess that backs up the complaint about portability, if sscanf() doesn't behave that way on all platforms. But on MSVC its behavior is reasonable enough.

shakna 2 days ago [-]
sscanf has locale things that make it include implementation-defined behaviour, doesn't it?
tliltocatl 2 days ago [-]
Anything that isn't C.UTF-8 is totally broken anyway.
PhilipRoman 2 days ago [-]
Case in point: echo y | LANG=lv_LV.UTF-8 grep '[a-z]'
Kevin_Flynn 1 days ago [-]
> No portable way to detect string-to-float conversion errors in standard C

There isn't just 1 standard C, theres many. AI says theres 5.

There are 5 major ISO C standards: C90, C99, C11, C17/C18, and C23. C89 is essentially the same standard as C90 under its ANSI name, and C95 was an amendment rather than a separate major edition. The latest is C23.

POSIX, as you know, stands for "Portable Operating System Interface."

> posix doesn't give a shit about math_errhandling; it always requires the implementation to set errno if overflow or underflow occurs.

So, POSIX, the "Portable" interface, specifies that underflow/overflow errors are always reported.

The problem seems to be in the headline, not the spec.

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