Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Returning References from Member Functions
Return references to internal data safely and understand the dangers of dangling references.
Member Functions Returning References to Data Members
Getter functions typically return copies of member variables. But what if you want to allow modification through a reference?
Returning by value vs. by reference
Standard getter returns a copy:
class Box
{
private:
int contents{};
public:
int getContents() const
{
return contents; // Returns a copy
}
};
int main()
{
Box box{};
int value{ box.getContents() };
value = 100; // Modifies local copy, not box.contents
return 0;
}
Returning by reference allows direct access:
class Box
{
private:
int contents{};
public:
int& getContents()
{
return contents; // Returns a reference
}
};
int main()
{
Box box{};
box.getContents() = 100; // Modifies box.contents directly!
return 0;
}
The danger
Returning non-const references to private members defeats encapsulation:
class BankAccount
{
private:
double balance{};
public:
double& getBalance()
{
return balance; // BAD: Exposes internal state
}
};
int main()
{
BankAccount account{};
account.getBalance() = 1000000.0; // Bypasses all validation!
return 0;
}
This is essentially making the member public. The private access specifier becomes meaningless.
When it's acceptable
Returning const references to large objects avoids expensive copies:
class Library
{
private:
std::string bookTitle{};
public:
const std::string& getTitle() const
{
return bookTitle; // OK: const reference, read-only
}
};
The const prevents modification, so encapsulation remains intact. This is safe and efficient for large objects like strings.
Rvalue implicit objects and dangling references
When calling a member function on an lvalue object, the returned reference remains valid as long as the object exists. But what happens when the implicit object is an rvalue (a temporary)?
Rvalue objects are destroyed at the end of the full expression in which they are created. Any references to members of that rvalue become dangling at that point.
#include <iostream>
#include <string>
class Employee
{
private:
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; }
};
// Returns an Employee by value (the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e{};
e.setName(name);
return e;
}
int main()
{
// Case 1: OK - use returned reference immediately in same expression
std::cout << createEmployee("Frank").getName() << '\n';
// Case 2: BAD - saving reference to member of rvalue creates dangling reference
const std::string& ref{ createEmployee("Garbo").getName() }; // reference becomes dangling
std::cout << ref << '\n'; // undefined behavior!
// Case 3: OK - copy the value to a local variable
std::string val{ createEmployee("Hans").getName() }; // makes a copy
std::cout << val << '\n'; // safe: val is independent of the temporary
return 0;
}
In Case 1, the temporary Employee returned by createEmployee() exists until the end of the full expression. We use the reference immediately to print the name, which is safe.
In Case 2, we save the returned reference into a local reference variable. The temporary Employee is destroyed at the end of the full expression (the initialization), leaving ref dangling. Using ref afterward is undefined behavior.
In Case 3, we copy the referenced value into a non-reference local variable. The copy is independent of the temporary, so it remains valid after the temporary is destroyed.
A reference to a member of an rvalue object can only be safely used within the full expression where the rvalue is created. Do not save such references for later use.
Prefer to use the return value of a member function that returns by reference immediately. If you need to persist the value, copy it into a non-reference local variable rather than saving the reference.
Chain modification pattern
Sometimes you want a fluent interface where operations can chain:
class Transform
{
private:
double value{};
public:
Transform& scale(double factor)
{
value *= factor;
return *this; // Return reference to self
}
Transform& add(double amount)
{
value += amount;
return *this;
}
double getValue() const { return value; }
};
int main()
{
Transform t{};
t.scale(2.0).add(5.0).scale(3.0); // Chaining
std::cout << t.getValue();
return 0;
}
Here, returning a reference to the object itself (*this) allows method chaining. This is fine because you're not exposing private data directly.
Returning references to members of complex types
For container-like classes, returning references allows element modification:
class IntArray
{
private:
int data[10]{};
public:
int& operator[](int index)
{
return data[index]; // Allows modification
}
const int& operator[](int index) const
{
return data[index]; // Read-only for const objects
}
};
int main()
{
IntArray arr{};
arr[0] = 42; // Modify through reference
std::cout << arr[0]; // Read value
return 0;
}
This is acceptable because:
- It matches expected container behavior
- Users understand they're modifying the container's elements
- You still control how elements are accessed (through
operator[])
Prefer returning by const reference for read-only access to large objects. Avoid returning non-const references to members as it usually breaks encapsulation. Return references to self (*this) for method chaining. For small types, return by value since copying is cheap. For large types, return by const reference to avoid expensive copies.
Summary
Returning by value vs. reference: Returning by value creates a copy of the member, while returning by reference provides direct access to the member variable itself.
The danger of non-const references: Returning non-const references to private members defeats encapsulation by allowing external code to modify internal state directly, bypassing validation and control mechanisms.
Const references are safe: Returning const references allows efficient access to large objects without copying while preventing modification, preserving encapsulation.
Rvalue objects and dangling references: When a member function returning a reference is called on an rvalue (temporary) object, the reference becomes dangling when the temporary is destroyed at the end of the full expression. Use such references immediately or copy the value to a local variable.
Method chaining: Returning a reference to *this enables fluent interfaces where multiple operations can be chained together in a single statement.
Container exceptions: For container-like classes (arrays, lists), returning references to elements is acceptable and expected behavior, matching how standard containers work.
Encapsulation's value lies in controlling access to internal state. When you return non-const references to private members, you bypass validation logic, prevent future implementation changes, and make debugging harder. Keep private members truly private by returning values or const references.
Returning References from Member Functions - Quiz
Test your understanding of the lesson.
Practice Exercises
Returning References
Understand when and how to return references from member functions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!