Ref qualifiers are one of those C++11 features that appear in textbooks, get a brief mention in conference talks, and then largely disappear from everyday code. Jens Weller recently revisited them and noted the same pattern: plenty of examples, few compelling use cases.
That framing is worth pushing back on. The use cases exist; they just require thinking about object lifetime more carefully than most C++ code demands.
The Syntax, Quickly
A ref qualifier goes after the parameter list and any cv-qualifiers:
class Widget {
public:
void process() &; // called only on lvalues
void process() &&; // called only on rvalues
};
Without a qualifier, a member function accepts both lvalues and rvalues, as it always has. The & qualifier restricts the call to lvalue objects; && restricts it to rvalues, including temporaries. The two can coexist as overloads, and both can be combined with const:
class Widget {
public:
void process() &;
void process() const &;
void process() &&;
void process() const &&;
};
Four overloads for one logical operation. That combinatorial explosion is part of why the feature sees limited use.
Where It Matters: Preventing Dangling References
The most concrete use case involves returning references or reference-like objects from member functions. Consider std::optional<T>. Its value() member returns a reference to the stored value, but if you call it on a temporary, you get a dangling reference:
auto& ref = std::optional<std::string>("hello").value();
// ref is dangling; the temporary is destroyed
The standard library addresses this by overloading value() with all four ref and const combinations:
template <typename T>
class optional {
T& value() &;
const T& value() const &;
T&& value() &&;
const T&& value() const &&;
};
When called on an rvalue optional, value() returns an rvalue reference, making it harder to accidentally capture a dangling lvalue reference. The same pattern appears in std::expected, range adaptors, and any class that hands out references to its own storage.
std::string_view suffers from a related problem. You can accidentally construct a view over a temporary string:
std::string_view sv = std::string("temporary");
// sv is immediately dangling
A well-designed string-owning class can use ref qualifiers to block this at compile time:
class StringOwner {
std::string data;
public:
std::string_view view() const & { return data; }
std::string_view view() const && = delete; // block rvalue calls
};
StringOwner make_owner();
auto sv = make_owner().view(); // compile error: deleted
Deleting the rvalue overload turns a runtime bug into a compile-time error. That is the most direct argument for ref qualifiers: they let you express lifetime constraints that const alone cannot capture.
Move Semantics in Method Chains
Builder patterns benefit from ref qualifiers in a more subtle way. A builder’s setter methods typically return *this by reference, enabling chaining. But if the builder itself is an rvalue, returning by reference is unnecessary; you could return by value and let the compiler elide the copy:
class QueryBuilder {
std::string query;
std::vector<std::string> filters;
public:
QueryBuilder& where(std::string condition) & {
filters.push_back(std::move(condition));
return *this;
}
QueryBuilder where(std::string condition) && {
filters.push_back(std::move(condition));
return std::move(*this);
}
std::string build() && {
return std::move(query);
}
};
auto result = QueryBuilder{}
.where("id > 0")
.where("active = true")
.build();
In this pattern, each method in the chain receives an rvalue and returns an rvalue, giving the optimizer a clear path to eliminate unnecessary copies. The lvalue overload remains for cases where you hold a named builder and accumulate conditions over multiple statements.
Without ref qualifiers, you either always copy or always return by reference, giving up precision in one direction.
The Four-Overload Problem
The real friction with ref qualifiers is the boilerplate. A single logical operation on a class may require four overloads to cover every combination of const and ref qualifier. In the standard library, this is manageable: the library is written once and used everywhere. In application code, maintaining four near-identical overloads for every accessor is impractical.
C++23 addresses this directly with the deducing this feature, standardized as P0847, which allows an explicit object parameter:
template <typename T>
class optional_like {
T storage;
public:
template <typename Self>
auto&& value(this Self&& self) {
return std::forward<Self>(self).storage;
}
};
A single template with an explicit this Self&& parameter covers all four cases. The type of Self carries both the constness and value category of the object, so std::forward propagates them correctly through to the return type. This is strictly more expressive than writing four ref-qualified overloads, and it compiles to the same machine code.
For new code targeting C++23, deducing this often replaces the four-overload pattern entirely. Ref qualifiers remain relevant when you need to delete specific overloads outright, or when you want genuinely distinct implementations for lvalue and rvalue cases rather than a unified template.
Why Most Code Ignores This
Several factors keep ref qualifiers out of everyday code. Application-level classes rarely hand out references to their internal storage in ways that create lifetime hazards; that problem is concentrated in library design. The standard library absorbs most of the complexity, so users encounter the feature only when reading headers.
Compilers do not warn when you bind the result of a member function call on a temporary to a long-lived reference. The bug is silent until runtime, which reduces the immediate pressure to add protective overloads.
There is also a non-obvious constraint: mixing ref-qualified and unqualified overloads of the same function in the same class is not allowed by the standard. That means adding ref qualifiers to an existing class is a breaking change if any code previously called those methods on rvalues without explicit qualification. Retrofitting is harder than it looks.
Finally, the feature was introduced in C++11 alongside move semantics, at a time when the community was already absorbing rvalue references, perfect forwarding, and the new ownership model. Ref qualifiers, which solve a narrower problem, did not receive as much attention, and that initial lack of momentum persisted.
When to Reach for Them
Ref qualifiers are worth considering in a few specific situations. If you are writing a class that returns references, iterators, or view types into its own storage, deleting the rvalue overloads prevents a category of lifetime bugs that are otherwise invisible until a crash. If you are writing a fluent interface that is performance-sensitive, the lvalue-by-reference and rvalue-by-value split eliminates copies across method chains.
For everything else, the cost in boilerplate and mental overhead generally exceeds the benefit, especially if your codebase is moving toward C++23 where deducing this handles the common cases more cleanly.
Ref qualifiers are not a poorly-designed feature. The problem they solve is real, and the feature solves it precisely. The narrowness of that precision is exactly why the feature exists at the margin of C++ idiom rather than at its center.