Most C++ developers who picked up the language after 2011 have encountered ref qualifiers at some point, usually in a header they did not write, looked them up, read a sentence like “controls overload resolution based on value category of the implicit object,” and moved on. The feature sits in an awkward spot: syntactically minor, semantically precise, and genuinely useful in maybe three or four scenarios that most code never encounters.
Jens Weller’s recent piece on isocpp.org is a good prompt to revisit them, because the standard examples you find online tend to show the mechanics without motivating the need. This post tries to fix that.
What Ref Qualifiers Actually Do
A member function in C++ has an implicit this parameter. Before C++11, you could qualify that parameter as const or volatile, but you could not express anything about its value category. Ref qualifiers fill that gap.
struct Buffer {
void consume() &; // only callable on lvalues
void consume() &&; // only callable on rvalues
};
The & overload is selected when the object is an lvalue: a named variable, a reference, anything with an address you can take. The && overload is selected when the object is an rvalue, meaning a temporary or a value you have explicitly cast with std::move. The rules come from the same overload resolution machinery as regular reference binding, applied to the implicit object parameter.
One constraint worth knowing early: you cannot mix a ref-qualified overload with an unqualified one for the same function name. Once you add any ref qualifier, every overload of that function must have one. cppreference documents this directly:
struct S {
void foo(); // error if you also declare
void foo() &; // this
};
The compiler rejects that combination. Either all overloads carry ref qualifiers, or none do.
The Getter Problem
The most practical use case is the getter that returns a non-trivially-copyable member. Consider the standard options without ref qualifiers:
// Option A: return by value -- always copies
std::string name() const { return mName; }
// Option B: return by const reference -- can never move out
const std::string& name() const { return mName; }
Option A is safe but wasteful when the caller wants to move the string elsewhere. Option B avoids the copy but prevents any move, even when the entire object is a temporary that nobody else references. Andreas Fertig’s writeup on this pattern shows the solution clearly:
class Person {
std::string mName;
public:
const std::string& name() const & { return mName; }
std::string name() && { return std::move(mName); }
};
Now person.name() returns a reference with no allocation. std::move(person).name() or Person{"Alice"}.name() moves the string out without copying. The caller’s intent determines which overload fires, and the cost matches the intent.
This pattern is not hypothetical. The C++ standard library uses it in std::optional::value(), which provides four overloads covering const, non-const, lvalue, and rvalue combinations:
constexpr T& value() &;
constexpr const T& value() const &;
constexpr T&& value() &&;
constexpr const T&& value() const &&;
That proliferation of overloads is the main ergonomic cost. Four signatures to express what one template could handle, which is part of what motivated C++23’s deducing-this feature.
Builder Patterns and Consuming Operations
A second concrete use case is the builder pattern, where method chaining produces a final object from a sequence of configuration steps. Without ref qualifiers, nothing stops this from compiling:
QueryBuilder builder;
auto& step = builder.where("age > 18");
step.where("country = 'DE'");
auto query = step.build(); // legal, but ownership is muddy
With ref qualifiers you can make the terminal step only callable on an rvalue, forcing the caller to consume the builder:
class QueryBuilder {
Query mQuery;
public:
QueryBuilder& where(std::string condition) & {
mQuery.addCondition(std::move(condition));
return *this;
}
Query build() && {
return std::move(mQuery);
}
};
auto query = QueryBuilder{}
.where("age > 18")
.where("country = 'DE'")
.build(); // build() fires the && overload on the temporary
Calling builder.build() on a named variable now fails to compile. The API surface enforces that you either own the builder through the chain or you do not get the result. The constraint is real, not conventional, and the compiler enforces it without any runtime cost.
You can go further and delete the lvalue overload of specific methods:
Query build() & = delete; // explicitly disallow lvalue usage
This makes the intent explicit in the header and produces a clear diagnostic if someone tries to call it wrong.
Preventing Dangerous Temporaries
A narrower but legitimate use case is blocking certain operations on temporaries entirely. Some methods do not make sense when called on a value that is about to be destroyed:
class Watcher {
public:
// Registering a callback from a temporary watcher produces
// a dangling reference. The deleted overload makes this a
// compile error instead of a runtime surprise.
Watcher& watch(Resource& r) &;
Watcher& watch(Resource& r) && = delete;
};
Without the deleted && overload, someone could write Watcher{}.watch(resource), which constructs a temporary watcher, registers the resource, and immediately destroys the watcher. The registration is useless and potentially dangerous. The deleted overload converts a latent runtime bug into a compile-time diagnostic.
Interaction with const and Overload Combinatorics
Ref qualifiers compose with const and volatile, which is where the boilerplate starts to accumulate. A fully qualified member function covering all combinations looks like:
struct S {
void method() &;
void method() const &;
void method() &&;
void method() const &&;
};
In practice, const && is rarely useful (it applies to const temporaries, which are uncommon), so most real code needs only three overloads. Three is still three.
This combinatorial pressure is part of the motivation for C++23’s deducing-this feature, proposal P0847. With an explicit self parameter, the same getter collapses into a single template:
class Person {
std::string mName;
public:
template <typename Self>
auto&& name(this Self&& self) {
return std::forward<Self>(self).mName;
}
};
The deduced Self type carries both the const qualifier and the value category, so one function covers all four cases. As the Microsoft C++ blog explains, it also enables recursive lambdas and cleans up CRTP patterns significantly.
For projects on C++23, deducing-this largely supersedes ref qualifiers for the getter pattern. But C++23 adoption is still uneven across codebases and toolchains, and ref qualifiers remain the right tool for simpler cases where you genuinely only need two overloads and do not want to pull in a template.
Comparison with Rust’s self Variants
If you write Rust, you already think in these terms. Rust methods explicitly declare how they take self:
impl MyType {
fn borrow(&self) { /* shared reference */ }
fn borrow_mut(&mut self) { /* exclusive reference */ }
fn consume(self) { /* takes ownership */ }
}
The consuming self pattern corresponds directly to the && ref qualifier in C++. When a Rust method takes self by value, the compiler marks the original binding as moved and rejects further use. C++‘s && qualifier only participates in overload resolution; it does not make the original variable inaccessible after the call. If you call an && method via std::move(x), nothing prevents you from accessing x afterward (though the object is in a valid but unspecified state).
Ref qualifiers give you the vocabulary to express the intent. They do not enforce the postcondition the way Rust’s ownership rules do.
When to Use Them
Ref qualifiers are worth adding when at least one of the following applies:
- You have a getter returning a non-trivially-copyable member and callers will sometimes move the owning object after the call. The
const &and&&pair removes a copy on the rvalue path. - You have a builder or factory type where consuming the object is the terminal step and you want the compiler to reject any other usage.
- You have methods that produce dangling references or logical errors when called on temporaries, and deleting the
&&overload turns those bugs into diagnostics.
For most other cases, the extra overloads are not worth the noise. A single const & getter is adequate, and adding ref-qualified overloads for no concrete benefit is the kind of premature generalization that makes headers harder to read.
The feature has been in the language since 2011 and is available on any compiler that supports modern C++. If your project is not yet on C++23, ref qualifiers are the only mechanism for this kind of value-category-based overloading. If you are on C++23 and need the full const/non-const/lvalue/rvalue matrix in one function, deducing-this is cleaner. For the common two-overload case, both approaches are roughly equivalent in verbosity, and ref qualifiers have the advantage of universal support.
Weller’s original point stands: the feature is typically shown without compelling motivation. The syntax is not the hard part. The hard part is recognizing the handful of situations where the semantics genuinely pay off.