Concepts / Returning Values from Tasks

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.

  • Programming
Interactive lab

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

  1. def name(params): creates a function object and binds the name to it; the body does not run yet.
  2. A call evaluates its arguments, opens a new frame, binds each parameter to its argument and runs the body from the top.
  3. return value ends the call immediately (lines after it never run) and sends the value back; the frame and its local names are discarded.
  4. 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.
  5. 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.
  6. 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.

Educational simulation

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?

  • It still holds its original value, because the task worked on a copy

  • It holds 42, because the task changed the very data the caller named

Reveal answer
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.

cpp
Notes on the code
  • 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_in changes the caller's value
value (caller'svariable)42 after the calltarget (parameter)a reference, not a copysame object
What changes when the task writes into target: the parameter and the caller's variable name the same object, so the write is visible through both names.
Before
int value = 0;
fill_in(value);
// value is 0 here, before the task runs its assignment

The caller's variable holds its original value, 0

After
int value = 0;
fill_in(value);
// value is 42 here, because target = 42 wrote into the caller's object

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

callertaskresult variable1. declare with initial value2. call, passing the variable by reference3. write the answer into it4. read the answer after the call returns
When the variable is declared, when it is modified, and when the result is read, in the order the caller, the task, and the variable interact.

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.

Worked example

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.

  1. 1
    Decide 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.

  2. 2
    Add 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.

  3. 3
    Read 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.

Result

The signature tells the reader which argument is input and which is output, and the caller receives the average through the result argument.

cpp
Output
7.5
Notes on the code
  • const std::vector<double>& marks the input as read-only
  • double& result_location names where the answer is deposited
  • The caller reads average only after the call completes
data argumentread, then overwritteninput argumentread onlyresult locationwritten oncetask computes from input, deposits answer
How the single non-const reference design and the const-input design with a separate result location route the data differently.

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.

QuestionNon-const reference doing both jobsConst input plus separate result location
Is the task expected to modify the data it receives?Yes, that is the purpose of the non-const referenceNo, the input is only examined
Does the answer belong in the input?Yes, the result overwrites the data the caller suppliedNo, 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 writtenYes, 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 surpriseRarely 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 like
    void print_total(std::vector<double>& values) that never changes values
    Why it fails

    The non-const reference promises a change that never happens, and a reader cannot tell from the call whether the input was altered.

    Do this instead

    Pass 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 like

    A task with a non-const reference parameter called without declaring a variable to hold the outcome

    Why it fails

    The answer is deposited into the caller's data, so the caller must name data that can receive it.

    Do this instead

    Declare 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 like

    Printing the result variable on the line before the task call

    Why it fails

    The task has not yet written its answer, so the variable still holds its earlier value.

    Do this instead

    Read 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

MEDIUM

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.

Related concepts

More concepts starting with R