10
$\begingroup$

Returning a value from a function like int add(int,int); allows for a subset of the functionality of simply passing a pointer void add(int,int,*int);.

We see a mixture of pointers and return values used for results in many C APIs which introduces additional complexity.

In a minimal language (like C) why would it bother including an extra keyword for return statements? I'm interested in anyones insight here.

I know why a language would allow for a break or return; without a value to allow early exiting, but returning a value is the aspect I'm curious about people's thoughts.


To give some context, I'm deciding on whether to include return statements in my own language.

$\endgroup$
3
  • $\begingroup$ I think this kind of misses the point of out parameters in C. Often, you use them for things like strings, so that the caller gets to decide how the memory is allocated. For example, char buffer[10]; vs a heap-allocated buffer. This isn't possible with just return values. Also, there are lots of other examples of there being a "less general" version of a feature in lots of languages: for loops (just a special case of while loops), switch statements (just a special case of multiple ifs, etc. $\endgroup$ Jun 27, 2023 at 15:52
  • $\begingroup$ If you don't care about memory allocation, you can always just return a structure in C with whatever contents you want (the same way some other languages would let you return multiple values or a tuple). $\endgroup$ Jun 27, 2023 at 15:53
  • 2
    $\begingroup$ Congratulations on being the first question to reach HNQ! $\endgroup$
    – Seggan
    Jun 27, 2023 at 19:50

5 Answers 5

13
$\begingroup$

Register optimization

Simply said, any value that's small enough to fit in one (or even a couple of) register will be quicker to return via one (usually RAX in AMD64 systems) than if the caller had to allocate space for the return value and the callee access said memory space.

$\endgroup$
4
  • $\begingroup$ Would a tool like LLVM be able to perform this register optimisation with void add(int,int,*int);? I would expect it could? $\endgroup$ Jun 26, 2023 at 14:12
  • $\begingroup$ @JonathanWoollett-light That seems unlikely, optimizers are rarely able to cross function boundaries especially with pointers, so unless the function is inlined it probably won't fix it by default. Even when inlined it may not remove the pointer, as that's quite a rare situation and hard to optimize without changing behavior. $\endgroup$
    – mousetail
    Jun 26, 2023 at 14:18
  • $\begingroup$ @JonathanWoollett-light in a non-inlined case I wouldn't expect it since you could be passing it a volatile pointer, a memory address to an existing struct or something along those lines. Inlining does optimize it though, at least in this contrived godbolt example: godbolt.org/z/KMvca16Mf $\endgroup$
    – kouta-kun
    Jun 26, 2023 at 14:32
  • 1
    $\begingroup$ An optimiser is not allowed to change how parameters are passed to functions, but a calling convention is -- for example the FASTCALL convention has rules about passing a certain number of register-sized parameters in registers instead of on the stack. Importantly, both sides have to agree to this in advance, typically by including a common header file or other interface specification that explicitly indicates that this convention should be used instead of the default. A new language/platform design could declare a register-based convention is the default, provided you have a fallback. $\endgroup$
    – Miral
    Jun 28, 2023 at 0:45
18
$\begingroup$

When a function call returns a value, that means a function call is an expression which can be used within other expressions where a value is expected. Consider an expression like f() + g() for example. If f and g had out-parameters instead of return values, then this could not be written as an expression; you would need to write something like

int f_out, g_out;
f(&f_out);
g(&g_out);
f_out + g_out

This is impractical, and so even when languages support out-parameters, generally you can also return values, and out-parameters are only used for the cases where returning a single value from a function isn't sufficient.


So it's not really a question of choosing between out-parameters (or pointers) vs. return values; you should generally have return values, and then decide if you want to also support out-parameters. The main argument against out-parameters is that they transmit function results as a side-effects ─ calling the function mutates the variable passed in, changing the program state.

One of the most consistent things in the evolution of programming language design over the last several decades has been to prevent side-effects, particularly mutation, either by forbidding them entirely or by enabling the programmer to avoid them where they are unwanted. It's widely considered that immutability and function purity are beneficial even in the imperative paradigm, since mutation is the source of whole categories of bugs.

$\endgroup$
2
  • $\begingroup$ "last several decades has been to prevent side-effects" dishonorable mention to C# which added out parameters in the middle of the functional programming explosion $\endgroup$
    – kouta-kun
    Jun 26, 2023 at 14:36
  • 2
    $\begingroup$ @kouta-kun ref and out are entirely different things in C#. Most out-params (not ref-params) I've seen have been used as an equivalent to Swift/Rust if let, i.e. if (Guid.TryParse(str, out var guid)). $\endgroup$
    – Bbrk24
    Jun 27, 2023 at 15:08
6
$\begingroup$

Clarity. Returns are easier to grok then out parameters.

I might be making some enemies here... But out parameters smell. And they smell worse then old fish. There is nothing inherently wrong with an out parameter... But there are a couple of downsides to using them.

Consider:

int x = foo();

what does this function do? no idea, but I know that it is returning an int which is assigned to x. That much is really clear. And with better function/result names I can learn everything I need to about the function by just looking at the function call. No need to inspect it.

what about if we have a function that uses an out parameter such as:

int x;
foo(&x);

What is happening here? no idea. What I know is that foo takes a pointer to x, I don't really know what it does with that pointer... maybe it assigns it a new value.. maybe x is supposed to have a value and foo takes an int pointer as an input so there is an error here... I cannot say without looking at foo's documentation.

Now increase the complexity, and create more complicated function calls. That require multiple inputs, have multiple outputs etc...

"But out parameters allow for multiple returned variables"... SRP (single responsibility principle), A function should do 1 thing. Multiple outputs are a sign that a function is doing too many things. If it really needs to return multiple values because they are related, why aren't you keeping those variables together as a struct to make certain that nothing is lost or mixed?

The subset of functionality offered by returns vs out parameters is a good thing, because it encourages better coding practices, makes code more readable, etc...

$\endgroup$
2
  • 1
    $\begingroup$ To an extent, this can be mitigated by having different operators for write-only vs read-write pointers, e.g. C# out vs ref. $\endgroup$
    – Bbrk24
    Jun 27, 2023 at 18:07
  • $\begingroup$ @Bbrk24 Yes, notably C has a keyword for read-only: const. But no keyword for write-only. $\endgroup$
    – Stef
    Jun 27, 2023 at 21:38
3
$\begingroup$

Various languages have, over the years, developed many different ways to handle returning values from functions -- often simultaneously. They tend to serve different needs.

A single return value is convenient because it allows easier composition of expressions that would otherwise have to be captured into variables and later operated on separately.

Output/reference parameters are convenient where a value is produced as a side effect (especially where not always needed). There's an interesting side argument here whether it's better to pass a null pointer in when a particular side output is not required -- this allows the callee to skip computation of the value, which might be a performance benefit; but also requires conditional logic, which might be a performance loss.

Reference parameters are convenient where you want your function to sometimes leave a value unchanged, or when you want the caller to control the memory allocation.

Modern language design seems more inclined to gravitate towards returning multiple values for the cases where a single function has multiple logical returns, typically combined with a destructuring assignment or similar to then decompose these into individual variables for later use.

Others argue that multiple return values are a code smell and should be rewritten to separate function calls, or introduce a new class that encapsulates the combined result (e.g. did you really want to return a bool,value pair or is that better expressed as an optional<T> or a ResultOrError<T> etc?). Typically this sort of thing only works well with generic/template types or it can lead to an explosion of tiny types, especially for one-off use cases.

Ultimately it's going to come down to a question of how strongly opinionated do you want your language to be, and whether it's a low-level language where people want to finely control memory allocation or a higher-level language where such things are more abstracted.

$\endgroup$
1
$\begingroup$

It is easier to read the code where arguments and results are possible to tell apart without viewing the signature of the function:

(r1, r2) = f(a1, a2)

is more easy to understand than something like f(a1, a2, r1, r2).

As seen in the example, multiple return values are also possible with tuples that can be immediately unpacked into separate variables (r1 and r2).

If r1 and r2 are more tightly coupled (like X and Y co-ordinates of the location, for instance), it is usually advocated that the function should return the properly defined structure instead, that would name the values for clarity. Tuples are more recommended when the values are not very related, like location and error code, for instance.

$\endgroup$
1
  • $\begingroup$ On the visibility front, in C# arguments are labeled with out at the call site, for out parameters. $\endgroup$
    – Pablo H
    Dec 12, 2023 at 19:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .