/* 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
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.
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() }
}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.