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.