[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. /* 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;
}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.
Of course C++ gives you the option to do any of the above, and still contains insane API contracts
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.
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
This is cool. Unfortunately it's also too late now. The machines have taken over programming.
Still had something as sane as:
if (double.TryParse("5.x5", out var val)) { Console.WriteLine(val); } else { Console.WriteLine("err"); }
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.Example:
#define E_DIVZERO -100
int divide(float a, float b, float *result) {
if (b == 0.0) return E_DIVZERO;
*result = a / b;
} 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.
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
> act on context structures
Same semantics but mutating context is much harder to track, so yes it is preferred to return 2 values.
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 ?
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.
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.
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 */ } 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); 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. 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.errno is a worse mistake than null pointers in general
> 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/...
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.
https://sixlettervariable.blogspot.com/2007/05/vax-floating-...
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.
https://www.exploringbinary.com/visual-c-plus-plus-strtod-st...
https://lemire.me/blog/2026/02/01/converting-floats-to-strin...
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.
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.
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?
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.
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.
> 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.
Isn't it the US that is the "different from everyone else" in this context?
Just how shut-in are you?
Regardless, it's goofy. Everybody should agree on one or the other, and it's too late for the other 73% to switch.
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()!
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.
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.
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).
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.
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.