Return by Reference and Return by Address in C++

In previous chapters 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 fine. But copying is typically expensive for class types (such as std::string). We can avoid making an expensive copy by utilizing passing by (const) reference (or pass 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.

std::string returnByValue(); // returns a copy of a std::string (expensive)

Return by Reference

In cases where we are 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 avoid a copy of the return value. To return by reference, we simply define the return value of the function to be a reference type:

std::string&       returnByReference(); // returns a reference to an existing std::string (cheap)
const std::string& returnByReferenceToConst(); // returns a const reference to an existing std::string (cheap)

Here is an academic program:

#include <iostream>
#include <string>

const std::string& getProgramName() // returns a const reference
{
    static const std::string s_programName { "Anony" }; // has static duration, destroyed at end of program

    return s_programName;
}

int main()
{
    std::cout << "This program is named " << getProgramName();

    return 0;
}

// Output
This program is named Anony

Because getProgramName() returns a const reference, when the line return s_programName is executed, getProgramName() will return a const reference to s_programName (thus avoiding making a copy). That const reference can then be used by the caller to access the value of s_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 (referencing an object that has been destroyed), and use of that reference will result in undefined behavior.

In the above program, because s_programName has static duration, s_programName will exist until the end of the program. When main() accesses the returned reference, it is actually accessing s_programName, which is fine, because s_programName won't be destroyed until later.

Now let's modify the above program to show what happens in the case where our function returns a dangling reference:

#include <iostream>
#include <string>

const std::string& getProgramName()
{
    const std::string programName { "Calculator" }; // now a non-static local variable, destroyed when function ends

    return programName;
}

int main()
{
    std::cout << "This program is named " << getProgramName(); // undefined behavior

    return 0;
}

The result of the program is undefined. When getProgramName() returns, a reference bound to local variable programName is returned. Then, because programName is a local variable with automatic duration, programName is destroyed at the end of the function. That means the returned reference is now dangling, and use of programName in the main() function results in undefined behavior.

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 or temporary by reference.

Let's take a look at an example where we return a temporary by reference:

#include <iostream>

const int& returnByConstReference()
{
    return 5; // returns const reference to temporary object
}

int main()
{
    const int& ref { returnByConstReference() };

    std::cout << ref; // undefined behavior

    return 0;
}

In the above program, returnByConstReference() is returning an integer literal, but the return type of the function is const int&. This results in the creation of a temporary reference bound to temporary object holding value 5. This temporary reference to a temporary object is then returned. The temporary object then goes out of scope, leaving the reference dangling.

By the time the return value is bound to another const reference (in main()), it is too late to extend the lifetime of the temporary object – as it has already been destroyed. Thus ref is bound to a dangling reference, and use of the value of ref will result in undefined behavior.

Don't return non-const local static variables by reference

In the original example above, we returned a const local static variable by reference to illustrate the mechanics of return by reference in a simple way. However, returning non-const static variables by reference is fairly non-idiomatic, and should generally be avoided.

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 }; // note: variable is non-const
    ++s_x; // generate the next id
    return s_x; // and return a reference to it
}

int main()
{
    const int& id1 { getNextId() }; // id1 is a reference
    const int& id2 { getNextId() }; // id2 is a reference

    std::cout << id1 << id2 << '\n';

    return 0;
}

//Output
22

This happens because id1 and id2 are referencing the same object (the static variable s_x), so when anything (e.g., getNextId()) modifies that value, all references are now accessing the modified value. Another issue that commonly occurs with programs that return as static local by const reference is that there is no standardized way to reset s_x back to the default state. Such programs must use a non-idiomatic solution, or can only be reset by quitting and restarting the program.

Avoid returning references to non-const local static variables.