- Learn C++ Test Site - https://test.learncpp.com -

8.10 — Return by reference and return by address

In previous lessons, we discussed that when passing an argument by value, a copy of the argument is made into the function parameter. For fundamental types (which are cheap to copy), this is fine. But copying is typically expensive for class types (such as std::string), and we can avoid paying this cost by passing by (const) reference or by address instead.

We encounter a similar situation when returning by value: a copy of the return value is passed back to the caller. If the return type of the function is a class type, this can be expensive.

Return by reference

In cases where we’re passing a class type back to the caller, we may (or may not) want to return by reference instead. Return by reference returns a reference that is bound to the object being returned, which avoids making a copy of the return value. To return by reference, we simply define the return value of the function to be a reference type:

Here is an academic program to demonstrate a value returned by reference:

Because getProgramName() returns a const reference, when the line return g_programName is executed, getProgramName() will return a const reference to g_programName (thus avoiding making a copy). That const reference can then be used by the caller to access the value of g_programName, which is printed.

The object being returned by reference must exist after the function returns

Using return by reference has one major caveat: the programmer must be sure that the object being referenced outlives the function returning the reference. Otherwise, the reference being returned will be left dangling, and use of said reference will result in undefined behavior.

In the program above, because g_programName is a global variable with static duration, programName will exist until the end of the program. When main() accesses the returned reference, it is actually accessing g_programName. This is fine because g_programName still exists in the scope of main().

Now let’s see what would happen in a case where a function returns a reference to a value that does not exist in the scope of the caller. Consider the following example that returns a local variable by reference:

What’s the result of this program? It’s indeterminate. When getProgramName() returns, a reference bound to local variable programName is returned. Then, because programName is an automatic variable, programName is destroyed at the end of the function. The means the returned reference is now dangling, and use of programName in the main function results in undefined behavior.

Modern compilers will produce a compile error if you try to return a local variable by reference (so the above program likely won’t even compile), but compilers sometimes have trouble detecting more complicated cases.

Warning

Objects returned by reference must live beyond the scope of the function returning the reference, or a dangling reference will result. Never return a local variable by reference.

Returning local static duration variables by reference

After seeing the above example, you may have had the thought that if we made the local variable programName a static variable, then it will have static duration, and we could return it by reference. And you would be correct. The following program compiles and functions as expected:

However, returning static variables by reference is fairly non-idiomatic, and should generally be avoided. Here’s a simplified example that illustrates why:

This program prints:

22

This happens because id1 and id2 are referencing the same object (the static variable x), so when anything (e.g. getNextId()) modifies that value, all references are now accessing the modified value.

While the above example is a bit silly, there are permutations of the above that programmers sometimes try for optimization purposes, and then their programs don’t work as expected.

Best practice

Avoid returning references to local static variables.

Returning const references to const global variables is sometimes done as a way to encapsulate access to a class type global variable. We discuss this in lesson 6.9 -- Why (non-const) global variables are evil [1]. When used intentionally and carefully, this is okay.

Assigning/initializing a normal variable with a returned reference makes a copy

If a function returns a reference, and that reference is used to initialize or assign to a non-reference variable, the return value will be copied (as if it had been returned by value).

In the above example, getNextId() is returning a reference, but id1 and id2 are non-reference variables. In such a case, the value of the returned reference is copied into the normal variable. Thus, this program prints:

12

Type deduction and reference return types

NOTE ABOUT HOW TYPE DEDUCTION DROP REFERENCES, MAKING THE ABOVE POSSIBLE

It’s okay to return reference parameters by reference

There are quite a few cases where returning objects by reference makes sense, and we’ll encounter many of those in future lesson. However, there is one useful example that we can show now.

If a parameter is passed into a function by reference, it’s safe to return that parameter by reference. This makes sense: in order to pass an argument to a function, the argument must exist in the scope of the caller. When the called function returns, that object must still exist in the scope of the caller.

Here’s is a simple example of such a function:

This prints:

Hello

In the above function, the caller passes in two std::string objects by const reference, and whichever of these strings comes first alphabetically is passed back by const reference. If we had used pass and return by value, we would have made up to 3 copies of std::string (one for each parameter, one for the return value). By using pass by reference/return by reference, we make no copies.

The caller can modify values through the reference

When a parameter is passed to a function by non-const reference, the function can use the reference to modify the value of the argument.

Similarly, when a non-const reference is returned from a function, the caller can use the reference to modify the value being returned.

Here’s an illustrative example:

In the above program, max() returns returns by reference whichever parameter has a larger value (in this case, y). The caller (main) then uses this reference to modify the value of that object to 7.

This prints:

57

Return by address

Return by address works almost identically to return by reference, except a pointer to an object is returned instead of a reference to an object. Return by address has the same primary caveat as return by reference -- the object being returned must outlive the scope of the function, otherwise the caller will receive a dangling pointer.

The major advantage of return by address over return by reference is that we can have the function return nullptr if there is no valid object found to return. For example, let’s say we have a list of students that we want to search. If we find the student we are looking for in the list, we can return a pointer to the object representing the matching student. If we don’t find any students matching, we can return nullptr to indicate a matching student object was not found.

The major disadvantage of return by address is that the caller has to remember to do a nullptr check before dereferencing the return value, otherwise a null pointer dereference may occur and undefined behavior will result. Because of this danger, return by reference should be preferred over return by address unless the ability to return “no object” is needed.

Best practice

Prefer return by reference over return by address unless the ability to return “no object” is important.


8.11 -- Introduction to program-defined types [2]
Index [3]
8.9 -- Pass by address [4]