Ref-qualification was added in c++11. In general, propagation of the value-category is incredibly useful for generic programming!
Semantically, ref-qualifications on functions help to convey the intent; acting on lvalue references or rvalues -- which is analogous to const or volatile qualifications of functions. These also parallel the behavior of struct members, which propagate the qualifiers and categories.
A great example of this in practice is std::optional, which provides the std::optional::value() function, which propagates the value-category to the reference on extraction:
auto x = std::move(opt).value(); // retrieves a T&&
This is analogous to member-access with structs, where the value-category is propagated on access:
struct Data {
std::string value;
};
auto data = Data{};
auto string = std::move(data).value; // expression yields a std::string&&
In terms of generic composition, this massively simplifies cases where the input may be an lvalue or an rvalue. For example, consider the case of using forwarding references:
// Gets the internal value from 'optional'
template <typename Optional>
auto call(Optional&& opt) {
// will be lvalue or rvalue depending on what 'opt' resolves as
return std::forward<Optional>(opt).value();
}
Without ref-qualification, the only way to accomplish the above code would be to create two static branches -- either with if constexpr or tag-dispatch, or some other means. Something like:
template <typename Optional>
auto call(Optional&& opt) {
if constexpr (std::is_lvalue_reference_v<Optional>) {
return opt.value();
} else {
return std::move(opt.value());
}
}
On a technical level, rvalue-qualifications on functions provides the opportunity to optimize code with move-constructions and avoid copies in a semantically clear way.
Much like when you see a std::move(x) on a value, you are to expect that x is expiring; it's not unreasonable to expect that std::move(x).get_something() will cause x to do the same.
If you combine && overloads with const & overloads, then you can represent both immutable copying, and mutating movements in an API. Take, for example, the humble "Builder" pattern. Often, Builder pattern objects hold onto pieces of data that will be fed into the object on construction. This necessitates copies, whether shallow or deep, during construction. For large objects, this can be quite costly:
class Builder {
private:
// Will be copied during construction
expensive_data m_expensive_state;
...
public:
auto add_expensive_data(...) -> Builder&;
auto add_other_data(...) -> Builder&;
...
auto build() && -> ExpensiveObject {
// Move the expensive-state, which is cheaper.
return ExpensiveObject{std::move(m_expensive_state), ...}
}
auto build() const & -> ExpensiveObject
// Copies the expensive-state, whcih is costly
return ExpensiveObject{m_expensive_state, ...}
}
...
};
Without rvalue-qualifications, you are forced to make a choice on the implementation:
- Do destructive actions like moves in a non-
const function, and just document the safety (and hope the API isn't called wrong), or
- Just copy everything, to be safe
With rvalue-qualifications, it becomes an optional feature of the caller, and it is clear from the authored code what the intent is -- without requiring documentation:
// Uses the data from 'builder'. May be costly and involves copies
auto inefficient = builder.build();
// Consumes the data from 'builder', but makes 'efficient's construction
// more efficient.
auto efficient = std::move(builder).build();
As an added benefit, static-analysis can often detect use-after-move cases, and so an accidental use of builder after the std::move can be better caught than simple documentation could.