upvote
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;
  }
reply
> 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.

reply
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.
reply
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

reply
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.

reply
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.
reply
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() }
    }
reply
I would not describe exceptions as return values. Speaking in a C context, exceptions are more like "longjmp" than "return".
reply
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)
reply
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.

reply
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.
reply
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.
reply
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

reply
> 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.

reply
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"); }

reply
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.
reply
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.
reply
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.
reply
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;
  }
reply
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.

reply
I agree, but you're missing the `return E_SUCCESS` at the end :-)
reply
Good catch. That was pre-coffee :)
reply
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

reply
> 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.

reply
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 ?

reply
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.

reply
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.
reply
Strictly speaking the unions in TS are untagged/set-theoretic, which is different from Rust or Zig.
reply
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.
reply
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.

reply
errno is global, but also _Thread_local, to help mitigate issues in a multithreaded environment. A kludge over a kludge...
reply
I was about to say that - it's really a mess
reply
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?
reply
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...
reply
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 */ }
reply
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);
reply

  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.
reply
Thank you for reminding me that, while Python is slow, there are still reasons to pick it over the alternatives.
reply
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.
reply
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.
reply