Skip to content
VLSI Mentor

C++ Part I · Module 6 · Functions

Return Values

Chapter 6.2 established that a function cannot change what its caller handed it, and listed three ways to be useful anyway. This is the first of them and by a wide margin the one to reach for: compute a result, hand it back, leave everything the caller owns exactly as it was. A function like that can be read in isolation, tested in isolation, and called without the reader checking what else it might have touched. The mechanism is simple enough that the chapter spends most of its length on two other things. The first is how much protection the compiler actually gives you, which is less than people assume — a function that forgets to return a value compiles, and so does one that returns a reference to an object that is about to stop existing, and in both cases what you get is a warning rather than a refusal. The second is the choice the frozen curriculum asks for by name: there are three different ways to get information out of a function, they are not interchangeable, and the right one is determined by what the signature tells a caller rather than by which form you happen to be used to writing.

Foundation17 min readC++Part IFundamentalsFunctions

Part I · Module 6 · Chapter 6.3 · Return Values

1. What return Does

Two things, and separating them helps:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int twice(int n)
    {
        return 2 * n;
    }

It produces the function's result. The expression after return is evaluated, and its value becomes the value of the call expression. Chapter 4.1 established that a call is an expression with a type; the return type in the declaration is that type.

It leaves the function immediately. Nothing after the return in that path runs, and every automatic object in the function is destroyed on the way out.

A void function returns no value, and return; with no expression is how you leave one early. Reaching the closing brace of a void function returns implicitly.

2. The Result Is Converted to the Return Type

The returned expression does not have to have the return type — it has to be convertible to it. Chapter 4.10's conversions apply, with the same consequences:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int truncating(double d)
    {
        return d;             // converted to int on the way out
    }

Measured: truncating(3.9) gives 3, with zero warnings. That is Chapter 3.7 §4's narrowing happening at the return statement instead of at an initialization, and Chapter 6.1 §4 showed the matching case at the argument end. A function boundary has two places a conversion can hide, and this is the second.

The silence is the same rule as everywhere else. Chapter 4.10 §7's pattern was that the compiler warns when it can see a specific value being damaged and is silent when the damage depends on a value it does not know. Here d is a parameter, so its value is unknown at compile time and nothing is said. Write return 3.9; directly and the warning returns — -Wliteral-conversion, changes value from 3.9 to 3.

Say it with a cast when you mean it, exactly as Chapter 4.10 §4 argued:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    return static_cast<int>(d);

3. Returning by Value Copies, and That Is Fine

A returned value is copied out of the function, because the object it came from is about to be destroyed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int sum(int a, int b)
    {
        int total = a + b;        // a local object
        return total;             // its value is copied out
    }

total ceases to exist when sum returns. The caller gets a value, not total.

For the fundamental types this costs nothing worth discussing. For a large type it is the same question Chapter 6.2 §6 measured — except that here the language and the compiler are allowed to eliminate the copy entirely in common cases, a set of rules called copy elision that belongs to Module 13. The practical consequence for now is the useful one: returning by value is not something to avoid on performance grounds. Returning a large object from a function is normal, idiomatic C++.

4. Forgetting to Return Is a Warning, Not an Error

Here is the first measurement, and it is less reassuring than you might hope.

A function declared to return a value, with a path that does not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int f(int n)
    {
        if (n > 0) return 1;
    }                              // what happens when n <= 0?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: non-void function does not return a value in all control paths [-Wreturn-type]

And one with no return at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int f() { }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: non-void function does not return a value [-Wreturn-type]

Both are warnings. The program compiles, links and runs. What the caller receives on the path with no return is not defined by the standard — flowing off the end of a value-returning function is undefined behaviour, in the precise sense Chapter 1.3 §5 gave it: the standard places no requirements on the program.

So the compiler has detected undefined behaviour, told you about it, and built the program anyway.

That is worth pausing on, because it is a different situation from the warnings in Module 5. There, the warned constructs were legal code whose appearance disagreed with its meaning. Here the warned construct is undefined behaviour the compiler can see, reported at the same severity as an unused variable.

The practical response is the one every serious project reaches: compile with -Werror, or at minimum -Werror=return-type, so this particular warning cannot be scrolled past. A function that sometimes returns nothing is not a style problem.

5. Making an Ignored Result a Warning: [[nodiscard]]

Chapter 4.1 §5 established that a value you ignore is simply discarded, and that the compiler warns about 19; while saying nothing about f();. For a function whose result is the entire point — a checksum, a status, a computed value — that silence is a problem.

C++17 added an attribute for it:

Azvya Education Pvt. Ltd.VLSI Mentor
nodiscard.cpp — complete program
#include <iostream>

[[nodiscard]] static int checksum(int a, int b) { return a ^ b; }
static int plain_checksum(int a, int b) { return a ^ b; }

int main()
{
    checksum(1, 2);            // result thrown away
    plain_checksum(1, 2);      // also thrown away
    std::cout << "done\n";
    return 0;
}

Measured: exactly one warning, on the first call.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nodiscard.cpp:8:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]

The second call is not mentioned. Both results were discarded identically; the only difference is that one function said its result matters.

This is the same pattern as Chapter 5.3's [[fallthrough]] and Chapter 4.10's named casts: when the language cannot tell a deliberate choice from a mistake, the fix is a spelling that records the intent. Here the intent being recorded is the author's, about their own function, for the benefit of every future caller.

Use it on a function whose result is the reason to call it, and especially on one that also has an effect — because that is the case where ignoring the result looks plausible.

6. Returning a Reference to a Local

The second measurement, and it is the one with teeth.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int& f()
    {
        int local = 5;
        return local;         // local stops existing when f returns
    }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: reference to stack memory associated with local variable 'local' returned [-Wreturn-stack-address]

A reference is another name for an object — Chapter 6.4's subject, and the mechanism Chapter 5.6 §5 already used. Returning one is returning a name for something, and here the something is destroyed before the caller can look at it. Using the result reads an object that no longer exists.

This is also a warning only. The compiler identified the problem precisely, named the variable, and produced a working executable.

The general subject — every way a reference or pointer can outlive what it refers to — is Chapter 8.8, which enumerates them. For this chapter the rule is narrow and sufficient:

Return by value from a function. Return a reference only when you can name the object it refers to and say why that object outlives the call.

At this point in the curriculum the honest answer is that you cannot usually say that, so the rule reduces to: return by value. Chapter 11.3 is where returning a reference becomes a normal thing to do, because a member function can return a reference to a member of an object the caller already owns.

7. Three Ways to Get Information Out

Now the chapter's second half, which the frozen purpose asks for by name.

Azvya Education Pvt. Ltd.VLSI Mentor
outparam.cpp — complete program
#include <iostream>

static int by_return(int a, int b) { return a + b; }
static void by_out_param(int a, int b, int& result) { result = a + b; }
static bool divide(int a, int b, int& quotient)
{
    if (b == 0) return false;          // a status code, because failure is possible
    quotient = a / b;
    return true;
}

int main()
{
    std::cout << "by return      : " << by_return(3, 4) << '\n';
    int r = 0;
    by_out_param(3, 4, r);
    std::cout << "by out-param   : " << r << '\n';

    int q = 0;
    std::cout << "divide 8 by 2  : " << (divide(8, 2, q) ? "ok" : "failed") << ", q = " << q << '\n';
    q = -1;
    std::cout << "divide 8 by 0  : " << (divide(8, 0, q) ? "ok" : "failed") << ", q unchanged = " << q << '\n';
    return 0;
}

Measured output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
by return      : 7
by out-param   : 7
divide 8 by 2  : ok, q = 4
divide 8 by 0  : failed, q unchanged = -1

The int& parameter is a reference parameter, Chapter 6.4's subject, appearing here because an out-parameter cannot be demonstrated without one. Chapter 6.2 §7's table already pointed at it.

Three forms, then:

  • A return value. int by_return(int, int).
  • An out-parameter. void by_out_param(int, int, int&) — the caller supplies an object to be filled.
  • A status code plus an out-parameter. bool divide(int, int, int&) — the return value says whether it worked; the out-parameter carries the result.

8. Choosing Between Them

The curriculum's phrasing is "on the strength of the interface rather than habit", and the test is what a caller can tell from the signature alone.

Return valueOut-parameter
Caller can see it is an outputyes, in the return typeonly if they notice &
Usable in an expressionyesno — needs a prior statement
Result can be constyesno — must be assigned later
Works when the function can failneeds a second channelthe return value is free for a status
Caller must initialize something firstnoyes

Prefer a return value. It is visible in the signature, it composes into expressions, and it lets the caller write const auto total = sum(a, b); — which Chapter 3.9 and Chapter 5.2 §3 both argued for, because an object initialized once and never changed is one fewer thing to track.

Reach for an out-parameter when there is a reason, and there are two good ones. The function produces more than one result and no single type groups them yet — Chapter 9.7's structs and Chapter 21.4's std::pair both remove that reason later. Or the caller already owns a large object to be filled and copying it out would be waste.

Do not use an out-parameter because the function is void. That is habit, and it is the thing the purpose is warning against.

9. Reporting Failure

The third form exists because some functions can fail, and a single return value then has to carry two things.

The measured example shows the discipline that makes it work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
divide 8 by 0  : failed, q unchanged = -1

q was set to −1 before the call and is still −1 after it. A function that reports failure must leave its out-parameter alone, so a caller who checks the status and stops is never looking at a half-written result. A function that fails and overwrites the output is worse than one that does neither, because the output now looks like an answer.

The options widen considerably later, and naming them keeps this chapter honest about being a starting point rather than the whole story:

ApproachWhere
status code + out-parameterhere
a type that holds "value or nothing" (std::optional)Chapter 21.5
exceptionsModule 22

Each is right in different circumstances, and Module 22 makes the comparison properly. For now: if a function can fail, the signature should say so, and the caller should not be able to reach the result without passing the status.

10. What to Actually Do

Return a value unless there is a specific reason not to.

Compile with -Werror=return-type at minimum. A missing return is undefined behaviour reported as a warning.

Mark a function [[nodiscard]] when its result is the reason to call it.

Return by value, not by reference, until Chapter 11.3 gives you a case where the referenced object outlives the call.

Convert on purpose. return static_cast<int>(d); rather than letting the return type narrow silently.

Use an out-parameter for a reason — multiple results, or a large object the caller already owns — not because the function is void.

Leave an out-parameter untouched when you fail, and make the signature force the caller to check.

Do not avoid returning large objects. Copy elision makes it idiomatic; Module 13 explains why.

11. Mistakes Worth Making Once

Assuming a missing return is an error. Measured: -Wreturn-type, a warning, and the program builds.

Scrolling past that warning. It is undefined behaviour, not a style note.

Returning a reference to a local. Measured: -Wreturn-stack-address, also only a warning.

Expecting the compiler to catch every dangling case. It caught this one because it was obvious. Chapter 8.8 is the general problem.

Letting the return type narrow silently. return d; from an int function truncates.

Assuming an ignored result is warned. Measured: only the [[nodiscard]] one was.

Using an out-parameter by default. The caller must create and initialize an object first, and cannot see from the signature that it is an output.

Overwriting an out-parameter on the failure path. Then a half-written value is indistinguishable from an answer.

12. Interview Relevance

"What happens if a non-void function has a path with no return?" The answer that gets credit names it as undefined behaviour and adds that the compiler warns rather than refusing — so it is caught only if warnings are enabled or promoted to errors.

"Return value or out-parameter?" Argue from the interface. A return value is visible in the signature, composes into expressions, and allows const; an out-parameter requires the caller to create an object first and is invisible unless they notice the &. Then give the two cases where an out-parameter is right: several results with no type to group them, or a large caller-owned object to fill.

"Is returning a large object by value slow?" No — copy elision means the copy is usually not made, and returning by value is idiomatic. Candidates who still avoid it are working from pre-2011 advice.

13. What You Should Be Able to Say Now

  1. return produces the result and leaves the function immediately, destroying its automatic objects.
  2. The returned expression is converted to the return type — a second place a conversion can hide at a function boundary.
  3. Returning by value copies out of a dying object, and copy elision usually removes the copy, so returning large objects is idiomatic.
  4. A missing return is a warning, not an error: non-void function does not return a value in all control paths [-Wreturn-type].
  5. Flowing off the end of a value-returning function is undefined behaviour the compiler reported at warning severity — hence -Werror=return-type.
  6. C++17 [[nodiscard]] makes an ignored result a warning. Measured: exactly one warning, on the marked function only.
  7. That is Chapter 5.3 and 4.10's pattern again — a spelling for the deliberate case so the accidental one shows.
  8. Returning a reference to a local is also only a warning: reference to stack memory associated with local variable 'local' returned.
  9. So: return by value until Chapter 11.3; Chapter 8.8 enumerates the general dangling problem.
  10. Three ways out: return value, out-parameter, status code plus out-parameter — all measured.
  11. Prefer the return value, because it is visible in the signature, composes into expressions, and can be const.
  12. Use an out-parameter for several results or a large caller-owned object, never because the function is void.
  13. Measured: a failing call left its out-parameter at −1 — a function that fails must not write its output.
  14. By-value parameters plus a return value is the only combination where the signature tells the whole story.

14. Exercises

Exercise 1 — what comes back? For each, give the value received by the caller: (a) int f() { return 3.9; } (b) double f() { return 7 / 2; } (c) int f() { return 7 / 2.0; } (d) char f() { return 321; }.

Exercise 2 — find the missing path. Add the missing return and say what the function returned without it: int sign(int n) { if (n > 0) return 1; else if (n < 0) return -1; }.

Exercise 3 — promote the warning. Give the exact flag that turns only the missing-return warning into an error, and explain in one sentence why this warning in particular deserves it.

Exercise 4 — mark it. Which of these should be [[nodiscard]], and why: (a) int checksum(const Block&) (b) void reset(Block&) (c) bool try_open(const char* path) (d) int pop() on a container that also removes the element.

Exercise 5 — explain the warning. int& largest(int a, int b) { int m = a > b ? a : b; return m; } warns. Say what the caller would be reading, give the one-word fix to the return type, and name the chapter that covers the general problem.

Exercise 6 — choose the form. For each, choose a return value, an out-parameter, or a status plus out-parameter, and justify from the signature: (a) add two numbers (b) parse a string that may be malformed (c) fill a 16 KB buffer the caller already owns (d) return both a quotient and a remainder (e) ask whether a register bit is set.

Exercise 7 — the failure contract. Rewrite so a caller who ignores the status cannot mistake a failure for an answer, and say what rule your version follows: int parse(const char* s, bool& ok);

15. Summary

  • return produces the result and leaves immediately; the expression is converted to the return type.
  • Returning by value copies out, and copy elision usually removes it — returning large objects is normal.
  • A missing return is a warning, and the behaviour is undefined. Use -Werror=return-type.
  • [[nodiscard]] makes an ignored result a warning — measured: one warning, on the marked function only.
  • Returning a reference to a local is also only a warning. Return by value until Chapter 11.3; Chapter 8.8 has the general case.
  • Three ways out: return value, out-parameter, status plus out-parameter.
  • Prefer the return value — visible in the signature, composable, const-able.
  • Out-parameters are for several results or a large caller-owned object.
  • Measured: a failing call left q at −1. Failure must not write the output.
  • By value in, value out is the only combination where the signature tells the whole story.

This chapter's recommendation has a boundary, and the next chapter is on the other side of it. When a function's entire job is to change something the caller owns — reset it, fill it, sort it — there is no result to return, and a by-value parameter is working on a copy that is thrown away. Chapter 6.2 proved that with two addresses. The next chapter prints those same two addresses again for a function that takes its parameter differently, and gets a different answer: the same number twice.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.