Returning Values from Tasks
A task can hand back a result by modifying data that the caller passed by non-const reference, which is a less obvious route than a return value.
Try it: Defining and Calling Functions
What def and a call really do: def only creates a function object, a call binds the arguments to the parameters in a new frame and runs the body, return ends the call at once and its value replaces the call expression in the caller — while a function that only prints gives back None.
How it works
- def name(params): creates a function object and binds the name to it; the body does not run yet.
- A call evaluates its arguments, opens a new frame, binds each parameter to its argument and runs the body from the top.
- return value ends the call immediately (lines after it never run) and sends the value back; the frame and its local names are discarded.
- In the caller, the call expression is replaced by the returned value, which can be stored, used in a larger expression or passed to another call.
- A body that ends without return (e.g. one that only prints) makes the call evaluate to None — so x = f() stores None and None + 1 raises TypeError.
- Calling a name before its def has run raises NameError: the function does not exist yet.
Default run (18 steps): The program is ready. Python runs it from the top, one statement at a time. Idea: the value a call returns is what the caller stores. … The program finished. Console: 8.
Simplified: Four fixed, hand-written programs; you edit the literal arguments, switch the body between return and print, and move the def after the calls. The lab's own small evaluator runs them (your input is never executed as code); every variant was run in CPython 3.12 and the lab reproduces its sys.settrace line/call/return/exception events, its output and its error message. Functions are shown as <function name> without a memory address.
Loading the simulation…
A Result Without a Return Value
A task usually exists to produce something, and the caller needs that something back. The most familiar route is the return value: the task computes an answer and hands it to the caller when it finishes. This article looks at a different route. Instead of sending the answer out through the return value, the caller supplies a reference to data that already exists, and the task writes its outcome into that data.
What do you think happens?
A task receives a reference to the caller's int variable, sets that reference to 42, and returns nothing. After the call, what does the caller's variable hold?
Reveal answer
It holds 42, because the task changed the very data the caller named
A non-const reference does not create a private copy. The task is working on the caller's own data, so the change is visible to the caller once the task finishes.
Two Routes Out of a Task
There are two ways an answer can leave a task. In the first, the answer travels out through the return value, and the caller receives it as the value of the call. In the second, the caller passes in a reference to existing data, and the task writes its answer into that data. Both routes deliver the same kind of information, but the second one sends it out through an input that the caller handed over rather than through the return value.
The source describes this second route as a somewhat sneaky way of returning a result. It is not unusual, so you will meet it in real code, but it is not usually the first choice a careful designer reaches for. Understanding it means understanding what a reference does to the argument it names.
How a Non-Const Reference Carries a Result
When an argument is passed by non-const reference, the task is not working on a private copy. It is working on the very data the caller named. Any change the task makes to that data is therefore visible to the caller once the task finishes. The caller does not need the task to return anything, because the caller's own variable already holds the outcome.
- The & after int in the parameter marks target as a non-const reference
- The call
fill_in(value)passes the caller's own variable, not a copy of it - The assignment inside
fill_inchanges the caller's value
int value = 0;
fill_in(value);
// value is 0 here, before the task runs its assignmentThe caller's variable holds its original value, 0
int value = 0;
fill_in(value);
// value is 42 here, because target = 42 wrote into the caller's objectThe same variable holds 42, because the task modified the data it was given by reference
Reading Intent from the Signature
The source ties the decision to intent. A non-const reference should be used only when the task is expected to modify the value of the data it refers to. The non-const reference is the signal that modification is part of the job. If you read a parameter list and see a non-const reference, you should expect the task to change that argument.
The reverse matters just as much. If a task is only supposed to read its input, a non-const reference promises a change that never belongs there. A reader of the call has no way to tell, from the call alone, whether the input was altered. The signature is the only place that reader can look for that information, so the signature must say what the task does.
Call Order Step by Step
Work through the order of events in a call that returns a result through an output argument. The caller declares the variable first, the task modifies it during the call, and the caller reads the result only after the call completes. Each step depends on the one before it, so the order is part of the technique.
The Alternative: Const Input and a Result Location
The source presents a less obscure technique for the same goal. The input data is passed by const reference, so the task can read it but is not meant to change it. A second argument is then added, and it names the place where the result should be deposited. The task writes the answer into that place. The flow of the answer is visible from the parameter list: one argument is marked as something the task will only examine, and the other is marked as something the task will fill.
Reporting the average of measurements
A task must take a list of measurements and report their average to the caller. Use the const-input, separate-result design.
- 1Decide whether the input changes
The task only needs to read the measurements to compute the average, so the list should be passed by const reference.
- 2Add a separate result argument
The average is the answer, and the caller needs it back. Add a second argument that names where the answer should be deposited.
- 3Read the parameter list
The const marks the measurements as read-only input. The non-const result argument marks the place the task will fill. The data flow is visible without reading the body.
The signature tells the reader which argument is input and which is output, and the caller receives the average through the result argument.
7.5- const std::vector<double>& marks the input as read-only
- double&
result_locationnames where the answer is deposited - The caller reads average only after the call completes
When Each Design Fits
The choice is not about which design is correct in general. It depends on whether the task is expected to modify the data it receives, and on whether the answer belongs in the input at all. The comparison below sets out the trade-offs the source points to.
| Question | Non-const reference doing both jobs | Const input plus separate result location |
|---|---|---|
| Is the task expected to modify the data it receives? | Yes, that is the purpose of the non-const reference | No, the input is only examined |
| Does the answer belong in the input? | Yes, the result overwrites the data the caller supplied | No, the answer goes to its own location |
| Can a reader see the flow from the parameter list? | Only partly, since a non-const argument may be read or written | Yes, const marks the input and the other argument marks the output |
| When is it a poor choice? | When the task only reads its input, because the change is then a surprise | Rarely a poor choice for a result the caller needs back |
Trade-offs between the two designs for returning a result from a task
Mistakes Beginners Make
Common mistake Making a parameter non-const when the task only reads it
What it looks likevoid print_total(std::vector<double>& values) that never changes valuesWhy it failsThe non-const reference promises a change that never happens, and a reader cannot tell from the call whether the input was altered.
Do this insteadPass the input by const reference, so the parameter list says it is read-only.
Common mistake Expecting a result to appear without a reference to receive it
What it looks likeA task with a non-const reference parameter called without declaring a variable to hold the outcome
Why it failsThe answer is deposited into the caller's data, so the caller must name data that can receive it.
Do this insteadDeclare the variable first, pass it to the task, and read it after the call.
Common mistake Reading the result before the call has finished
What it looks likePrinting the result variable on the line before the task call
Why it failsThe task has not yet written its answer, so the variable still holds its earlier value.
Do this insteadRead the result only after the call returns.
Common mistake Choosing the output-argument route without asking whether the answer belongs in the input
Designing a Task That Reports a Result
A task must take a list of exam scores and report the highest score to the caller. Decide between the non-const reference design and the const-input design with a separate result location. Write the parameter list and explain which argument is input and which is output.
Hints
- Ask whether the task is expected to modify the list of scores.
- Ask whether the answer belongs in the input or in its own location.
- Check whether a reader of the call could tell where the result goes without reading the body.
Key Takeaways
- A task can hand back a result by modifying data the caller passed by non-const reference, because the task works on the caller's own data rather than a copy.
- A non-const reference should be used only when modifying the argument is part of the task's job, since it signals that change to every reader of the call.
- The less obscure alternative passes the input by const reference and passes a separate argument that names where the result should be deposited.
- Separating input from result location makes the flow of the answer visible from the parameter list alone, which makes the design easier to follow.