A dangling reference is one that refers to an object that has been destroyed. When a reference is formed to an object's memory, the reference itself stores no information about when the object will cease to exist or when that memory will be reallocated for some other use.
As a result, accessing a reference after the object to which it refers has gone away can result in unexpected and dangerous behavior.
A common cause of dangling references is returning a reference to a local variable. In this situation, a local object is created within the function and then a reference to that object is returned to the caller.
While the local object exists, the reference to it can be used perfectly fine.
However, upon returning from the function, the local object is destroyed and its associated memory may be reallocated for a new purpose.
When the caller tries to access the returned reference, any subsequent operations are in undefined territory: the program may crash, report an incorrect value, corrupt unrelated data, or the behavior could seem entirely random depending on optimization levels or operating system usage. C++ does not offer any protections or warnings in such cases, leaving these potential errors up to the programmer to discover and prevent.
Consider the following function, which creates a local object and returns a reference to it:
#include <string>
// Return a reference rather than a separate copied object
std::string& buildbad_string()
{
std::string local{"temporary string"};
return local; // Undefined Behavior!
}
As soon as buildbadstring returns, the local string object it created is destroyed, and the reference returned is left dangling. When the caller uses that reference:
std::string& danglingref = buildbad_string();
// If we now read from dangling_ref, it's Undefined Behavior!
// For example, this might crash the program, print garbage, etc.
A C++ reference is simply an alias for an existing object. It does not manage the lifetime of an object. Once a local object goes out of scope, it's destroyed, and its associated memory is now available for anything the compiler chooses to do with it.
The reference, which previously named the object, now points to an invalid address.
Accessing it is undefined behavior. No check can be made against the reference's validity, since there's nothing to check against that remains valid after the function returns.
The correct approach when a function must return an object created within it is to return it by value:
#include <string>
std::string buildgood_string()
{
std::string local{"safe string"};
return local; // Safe
}
When a value is returned, a copy (or a move in modern C++) of the local object is made, and this copy is given to the caller. The original local object can then be safely destroyed because the caller has its own independent copy.
std::string goodstring = buildgood_string(); // Safe
// good_string now has its own memory and lifetime.
A function returning a reference to its local data simply gives the caller an alias for that memory. When the function ends, the object that memory previously held is destroyed.
Any reference which was supposed to name that object now points to invalid memory, causing undefined behavior when accessed. The memory that the reference points to may have been immediately reallocated for a different use.
By returning by value, a copy or moved version of the local object's data is created in the caller's scope. This new object then lives within the caller's context, and the local object used to generate the return value can be safely discarded.
There are no dangling references, and the caller possesses an independent, valid object.
#include <iostream>
#include <string>
std::string& buildbad_string()
{
std::string local{"dangerous string"};
return local;
}
std::string buildgood_string()
{
std::string local{"safe string"};
return local;
}
int main()
{
// std::string& danglingref = buildbad_string();
// std::cout << danglingref << '\n';
std::string goodstring = buildgood_string();
std::cout << goodstring << '\n';
goodstring = "changed safely";
std::cout << goodstring << '\n';
return 0;
}
When a return by value is assigned to a variable in the caller's scope, the caller's variable is given its own dedicated storage. This guarantees that changes to the caller's variable do not affect any remnants of the original function's local data and that no references are left dangling, pointing to outmoded or reused memory locations.
The fundamental rule is this: return a reference only if the referenced object's lifetime is guaranteed to be as long as the returned reference's potential lifespan.
This is never true for a local variable within a function.
Exercise: Dangling ReferencesStarter code, hints and solutionOpenClose
Practice: Dangling References
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Keep the safe by-value string factory. Bind a reference to the live goodstring, assign the text "updated through a live reference" through that alias, and print the original. Explain why this differs from returning a reference to a factory-local string.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
#include <string>
std::string buildgood_string()
{
std::string local{"safe string"};
return local;
}
int main()
{
std::string goodstring = buildgood_string();
std::cout << goodstring << '\n';
goodstring = "changed safely";
std::cout << goodstring << '\n';
return 0;
}Hints
buildgood_string returns an owning string by value. The goodstring in main remains alive after the factory returns, so a reference bound to it can still be used.
Bind the new reference to goodstring in main, not to the factory-local variable. Assignment through that alias then has a live target.
Worked solution
The unsafe reference-returning function is removed from the runnable answer. buildgood_string returns an owning string by value. The new reference alias refers to goodstring, which remains alive, and assigning through alias changes that original string safely.
#include <iostream>
#include <string>
std::string buildgood_string()
{
std::string local{"safe string"};
return local;
}
int main()
{
std::string goodstring = buildgood_string();
std::cout << goodstring << '\n';
goodstring = "changed safely";
std::cout << goodstring << '\n';
std::string& alias = goodstring;
alias = "updated through a live reference";
std::cout << goodstring << '\n';
return 0;
}
Example output
safe string
changed safely
updated through a live reference
0 comments
No comments yet. Be the first.