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.

For Context
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.

For advanced readers
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

Best Practice
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.