Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Returning References and Pointers from Functions
Return references or pointers from functions without copying, while avoiding dangling issues.
When passing arguments by value, C++ creates a copy of each argument for the function to work with. For simple types like integers, this copying is efficient. However, for complex types like std::string, copying can be resource-intensive. We avoid this overhead by passing arguments by reference or by address, which allows the function to work directly with the original object.
A similar situation occurs when returning values from functions: returning by value creates a copy that gets passed back to the caller. When the return type is a complex class type, this copying can significantly impact performance.
std::string buildPlayerName(); // returns a copy of a std::string (resource-intensive)
Return by reference
Return by reference allows a function to return a reference bound directly to an object, eliminating the need to create a copy. To return by reference, declare the function's return type as a reference type:
std::string& getPlayerName(); // returns a reference to an existing std::string (efficient)
const std::string& getPlayerNameConst(); // returns a const reference (efficient)
Here's a practical example:
#include <iostream>
#include <string>
const std::string& getGameTitle() // returns a const reference
{
static const std::string s_title{ "Dragon Quest XI" }; // static duration, exists until program ends
return s_title;
}
int main()
{
std::cout << "Now playing: " << getGameTitle();
return 0;
}
This displays:
Now playing: Dragon Quest XI
Since getGameTitle() returns a const reference, executing return s_title returns a const reference bound to s_title, avoiding any copying. The caller can then use this reference to access s_title's value for output.
The returned object must outlive the function
Using return by reference comes with a critical requirement: the object being referenced must continue to exist after the function returns. If the referenced object is destroyed when the function ends, the returned reference becomes dangling, and using it leads to undefined behavior.
In our previous example, s_title has static duration, meaning it persists until the program terminates. When main() uses the returned reference, it's safe because s_title won't be destroyed until the program ends.
Now let's examine what happens when a function returns a dangling reference:
#include <iostream>
#include <string>
const std::string& getGameTitle()
{
const std::string title{ "Dragon Quest XI" }; // local variable, destroyed when function exits
return title;
}
int main()
{
std::cout << "Now playing: " << getGameTitle(); // undefined behavior
return 0;
}
This program exhibits undefined behavior. When getGameTitle() returns, it returns a reference bound to local variable title. Since title has automatic duration, it's destroyed when the function ends. The returned reference is now dangling.
Modern compilers typically warn when you attempt to return a local variable by reference.
Objects returned by reference must persist beyond the function's scope, or a dangling reference will result. Never return a non-static local variable or temporary by reference.
Lifetime extension doesn't cross function boundaries
Consider returning a temporary by reference:
#include <iostream>
const int& returnTempReference()
{
return 42; // returns const reference to temporary object
}
int main()
{
const int& score{ returnTempReference() };
std::cout << score; // undefined behavior
return 0;
}
In this program, returnTempReference() returns an integer literal, but the function's return type is const int&. This creates and returns a temporary reference bound to a temporary object holding value 42. The temporary object then goes out of scope, leaving the reference dangling.
Here's a subtler example that fails similarly:
#include <iostream>
const int& passThrough(const int& value)
{
return value;
}
int main()
{
// scenario 1: direct binding
const int& direct{ 42 }; // extends lifetime
std::cout << direct << '\n'; // safe
// scenario 2: indirect binding
const int& indirect{ passThrough(42) }; // binds to dangling reference
std::cout << indirect << '\n'; // undefined behavior
return 0;
}
In scenario 2, a temporary object is created to hold value 42. The function simply returns this reference to the caller. Because this isn't a direct binding (the reference passed through a function), lifetime extension doesn't apply. This leaves indirect dangling.
Reference lifetime extension does not work across function boundaries.
Avoid returning non-const static local variables by reference
Returning non-const static local variables by reference is non-idiomatic and should generally be avoided:
#include <iostream>
#include <string>
const int& getNextId()
{
static int s_id{ 1000 }; // note: variable is non-const
++s_id;
return s_id;
}
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;
}
This displays:
10021002
This occurs because id1 and id2 both reference the same object (the static variable s_id). When anything modifies that value, all references access the modified value.
This can be fixed by making id1 and id2 normal variables (not references) so they store copies of the return value.
Avoid returning references to non-const local static variables.
Returning a const reference to a const local static variable is occasionally done when the local variable is expensive to create. This is uncommon.
Initializing a normal variable with a returned reference creates a copy
When a function returns a reference and that reference initializes or assigns to a non-reference variable, the return value gets copied (as if returned by value).
#include <iostream>
#include <string>
const int& getNextId()
{
static int s_id{ 1000 };
++s_id;
return s_id;
}
int main()
{
const int id1{ getNextId() }; // id1 is a normal variable, receives a copy
const int id2{ getNextId() }; // id2 is a normal variable, receives a copy
std::cout << id1 << id2 << '\n';
return 0;
}
In this example, getNextId() returns a reference, but id1 and id2 are non-reference variables. The value of the returned reference gets copied into the normal variable. This displays:
10011002
Also note that if a function returns a dangling reference, the reference is dangling before the copy occurs, leading to undefined behavior.
It's safe to return reference parameters by reference
There are several scenarios where returning objects by reference makes sense. One useful case:
When a parameter is passed to a function by reference, it's safe to return that parameter by reference. This makes sense: to pass an argument to a function, the argument must exist in the caller's scope.
#include <iostream>
#include <string>
// Takes two std::string objects, returns whichever is longer
const std::string& getLonger(const std::string& name1, const std::string& name2)
{
return (name1.length() > name2.length()) ? name1 : name2;
}
int main()
{
std::string heroName{ "Arthur" };
std::string villainName{ "Mordred the Betrayer" };
std::cout << getLonger(heroName, villainName) << '\n';
return 0;
}
This displays:
Mordred the Betrayer
Using pass by value and return by value would create up to 3 copies of std::string. Using pass by reference and return by reference avoids those copies.
It's safe for rvalues passed by const reference to be returned by const reference
When an argument for a const reference parameter is an rvalue, it's still safe to return that parameter by const reference.
This works because rvalues aren't destroyed until the end of the full expression in which they're created.
#include <iostream>
#include <string>
const std::string& identity(const std::string& text)
{
return text;
}
std::string buildGreeting()
{
return "Welcome, adventurer!";
}
int main()
{
const std::string message{ identity(buildGreeting()) };
std::cout << message;
return 0;
}
Here the rvalue is passed by const reference to identity() and returned by const reference to the caller before initializing message. Everything works correctly.
The caller can modify values through non-const references
When a non-const reference is returned from a function, the caller can use the reference to modify the returned value.
#include <iostream>
// takes two integers by non-const reference, returns the higher by reference
int& getHigher(int& score1, int& score2)
{
return (score1 > score2) ? score1 : score2;
}
int main()
{
int playerScore{ 85 };
int enemyScore{ 72 };
getHigher(playerScore, enemyScore) = 100; // sets the higher score to 100
std::cout << playerScore << enemyScore << '\n';
return 0;
}
In this program, getHigher(playerScore, enemyScore) determines which is larger. Since playerScore (85) is larger, it returns a reference to playerScore. The caller then assigns value 100 to this returned reference.
This displays:
10072
Return by address
Return by address works almost identically to return by reference, except it returns a pointer to an object instead of a reference. Return by address has the same primary caveat: the object being returned by address must outlive the function's scope, or the caller receives a dangling pointer.
The major advantage of return by address over return by reference is that the function can return nullptr when there's no valid object to return. For example, when searching through a list of players, if we find a match, we can return a pointer to the matching player. If no match is found, we can return nullptr.
The major disadvantage is that the caller must remember to check for nullptr before dereferencing, or a null pointer dereference may occur causing undefined behavior.
Prefer return by reference over return by address unless the ability to return "no object" (using `nullptr`) is important.
If you need the ability to return "no object" or a value (rather than an object), `std::optional` provides a good alternative.
Returning References and Pointers from Functions - Quiz
Test your understanding of the lesson.
Practice Exercises
Return by Reference and Address
Learn to return references and pointers from functions efficiently while avoiding dangling references. Understand when objects must outlive function scope and when it's safe to return references.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!