Overloading Based on Object Value Category
Provide different behavior for lvalue and rvalue objects using & and && qualifiers.
What Are Ref-Qualifiers?
Every member function call has an implicit object, the thing to the left of the dot. That object is an expression like any other, so it has a value category: it is either an lvalue or an rvalue. A ref-qualifier, added in C++11, lets you overload a member function on that value category, so one version runs when the object is a named variable and a different one runs when it is a temporary.
This is an optional lesson. A light read-through is enough to recognise the syntax when you meet it; nothing later in the course depends on it.
The syntax is a trailing & or &&, in the same position as a trailing const:
const std::string& getUsername() const & { return m_username; } // implicit object is an lvalue
std::string getUsername() const && { return m_username; } // implicit object is an rvalue
These are two distinct overloads, which is what makes the feature useful: they may differ in return type. Above, the lvalue version hands back a reference while the rvalue version hands back a copy.
The Problem This Solves
Ref-qualifiers exist for a specific class of bug. Recall from the lesson on member functions returning references to data members that a getter returning a reference is only safe while the object it came from is still alive. When the implicit object is a temporary, that can be a very short window.
The following program is broken, and is shown to demonstrate the mistake:
#include <iostream>
#include <string>
#include <string_view>
class Account
{
private:
std::string m_username{};
public:
Account(std::string_view username) : m_username{ username } {}
const std::string& getUsername() const { return m_username; }
};
Account createAccount(std::string_view username)
{
return Account{ username };
}
int main()
{
const std::string& ref{ createAccount("Bob").getUsername() };
std::cout << ref << '\n';
return 0;
}
This program is broken. The Account returned by createAccount is a temporary that dies at the end of the initializing expression, so ref is left referring to a member of an object that no longer exists, and reading it is undefined behavior. Recent compilers spot the pattern:
rq2.cpp: In function 'int main()':
rq2.cpp:23:24: warning: possibly dangling reference to a temporary [-Wdangling-reference]
23 | const std::string& ref{ createAccount("Bob").getUsername() };
| ^~~
rq2.cpp:23:42: note: 'Account' temporary created here
23 | const std::string& ref{ createAccount("Bob").getUsername() };
| ~~~~~~~~~~~~~^~~~~~~
Using the same call directly, as in std::cout << createAccount("Bob").getUsername() << '\n';, is fine, because the temporary survives to the end of that full expression.
So the getter has one job but two situations, and the ideal answer differs between them:
| Implicit object | Return by const reference | Return by value |
|---|---|---|
| Lvalue (a named variable) | Efficient, no copy, safe | Wasteful copy on the common path |
| Rvalue (a temporary) | Can dangle if the caller stores it | Safe |
Without ref-qualifiers you pick one compromise for both. The usual choice is to return by const reference, since implicit objects are usually lvalues, and to rely on the habit of consuming the result immediately.
Qualifying the Getter
With a ref-qualifier you stop compromising and write both:
#include <iostream>
#include <string>
#include <string_view>
class Account
{
private:
std::string m_username{};
public:
Account(std::string_view username) : m_username{ username } {}
const std::string& getUsername() const & { return m_username; }
std::string getUsername() const && { return m_username; }
};
Account createAccount(std::string_view username)
{
return Account{ username };
}
int main()
{
Account alice{ "Alice" };
std::cout << "From an lvalue: " << alice.getUsername() << '\n';
std::cout << "From an rvalue: " << createAccount("Bob").getUsername() << '\n';
const std::string saved{ createAccount("Carol").getUsername() };
std::cout << "Saved safely: " << saved << '\n';
return 0;
}
Output:
From an lvalue: Alice
From an rvalue: Bob
Saved safely: Carol
alice is a named variable, so the & overload runs and returns a reference with no copying. The two createAccount calls produce temporaries, so the && overload runs and returns a copy, which means the third case now stores a real std::string instead of a reference into a dead object.
When the implicit object is a non-const rvalue it is about to be destroyed anyway, so copying its member is wasteful. An overload declared
std::string getUsername() && { return std::move(m_username); } can move the member instead. It can sit alongside the const rvalue overload or replace it, since const rvalues are rare. std::move is covered in its own lesson later.
Rules Worth Knowing
- Ref-qualified and non-ref-qualified overloads of the same function cannot coexist. A function is either qualified in all its overloads or none of them.
- A lone
const &overload accepts rvalue implicit objects too, mirroring the way a const lvalue reference can bind to an rvalue. - Either overload can be
= deleted. Deleting the&&version is a way to forbid calling the function on temporaries altogether.
Why This Is Not the Default Advice
Do not reach for ref-qualifiers as a matter of course. Use the result of an access function straight away instead of storing the returned reference.
The feature is sound, but the cost of applying it broadly is high. Adding an rvalue overload to every reference-returning getter doubles the surface of the class to defend against a mistake that good habits already prevent. The rvalue overload also pays for a copy or move in cases where a reference would have been perfectly safe, such as printing the result immediately. On top of that, most C++ developers have never encountered the syntax, so code using it invites misreading, and the standard library itself largely leaves the feature alone.
Recognise it, understand what the trailing & and && mean when you meet them, and prefer the simpler discipline in your own classes.
Summary
What they are: a C++11 feature that overloads a member function on the value category of the implicit object, written as a trailing & for lvalues and && for rvalues.
Why they exist: a getter returning a reference to a member is unsafe when the implicit object is a temporary, because the reference outlives the object.
What they buy you: the lvalue overload can return by const reference for efficiency while the rvalue overload returns by value for safety, since the two overloads may have different return types.
Coexistence: ref-qualified and non-ref-qualified overloads of the same function are not allowed together, and a lone const & overload will bind to rvalues as well.
Deleting: marking an overload = delete bans that calling context entirely.
The recommendation: not a general best practice. Consume returned references immediately rather than storing them, and keep the class simple.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Overloading Based on Object Value Category - Quiz
Test your understanding of the lesson.
Practice Exercises
Ref-Qualified Getters
Implement a class with ref-qualified member functions that behave differently for lvalue and rvalue objects. This is an advanced optional exercise.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!