What are the most typical use cases of "rvalue references for *this" which the standard also calls reference qualifiers for member functions?
By the way, there is a really good explanation about this language feature here.
Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.
An rvalue reference can only bind to an rvalue (without a static_cast being involved), but it is otherwise an lvalue of type rvalue reference. The fact it is of type rvalue reference only matters during its construction, and if you do decltype(variable_name) . It is otherwise just another lvalue of reference type.
rvalue reference and lvalue reference are categories of references. Inside a declaration, T x&& = <initializer expression> , the variable x has type T&&, and it can be bound to an expression (the ) which is an rvalue expression. Thus, T&& has been named rvalue reference type, because it refers to an rvalue expression.
You probably haven't seen any practical code that uses const rvalue references ( const T&& ). And that's not really surprising. The main purpose of rvalue references is to allow us to move objects instead of copying them. And moving the state of an object implies modification.
When called, each member function has an implicit object parameter that *this
references.
So (a) these normal function overloads:
void f(const T&);
void f(T&&);
when called like f(x)
; and (b) these member function overloads:
struct C
{
void f() const &;
void f() &&;
};
when called like x.f()
- both (a) and (b) dispatch with similar viability and ranking.
So the use cases are essentially the same. They are to support move semantic optimization. In the rvalue member function you can essentially pillage the objects resources because you know that it is an expiring object (is about to be deleted):
int main()
{
C c;
c.f(); // lvalue, so calls lvalue-reference member f
C().f(); // temporary is prvalue, so called rvalue-reference member f
move(c).f(); // move changes c to xvalue, so again calls rvalue-reference member f
}
So for example:
struct C
{
C operator+(const C& that) const &
{
C c(*this); // take a copy of this
c += that;
return c;
}
C operator+(const C& that) &&
{
(*this) += that;
return move(*this); // moving this is ok here
}
}
Some operations can be more efficient when called on rvalues so overloading on the value category of *this
allows the most efficient implementation to be used automatically e.g.
struct Buffer
{
std::string m_data;
public:
std::string str() const& { return m_data; } // copies data
std::string str()&& { return std::move(m_data); } // moves data
};
(This optimisation could be done for std::ostringstream
, but hasn't been formally proposed AFAIK.)
Some operations don't make sense to call on rvalues, so overloading on *this
allows the rvalue form to be deleted:
struct Foo
{
void mutate()&;
void mutate()&& = delete;
};
I haven't actually needed to use this feature yet, but maybe I'll find more uses for it now that the two compilers I care about support it.
In my compiler framework (to be released Sometime Soon™), you pass items of information such as tokens into a compiler object, then call finalize
to indicate the end of stream.
It would be bad to destroy an object without calling finalize
, because it wouldn't flush out all its output. Yet finalize
can't be done by the destructor, because it can throw an exception, and likewise it's wrong to ask finalize
for more output if the parser is already aborting.
In the case when all the input is already encapsulated by another object, it's nice to pass input to an rvalue compiler object.
pile< lexer, parser >( output_handler ).pass( open_file( "source.q" ) );
Without special support, this must be incorrect because finalize
isn't getting called. The interface shouldn't let the user do such a thing at all.
The first thing to do is rule out the case where finalize
never gets called. The above example is disallowed if the prototype is adjusted with an lvalue ref-qualifier like this:
void pass( input_file f ) & {
process_the_file();
}
This makes room to add another overload which properly finalizes the object. It is rvalue ref-qualified so it is selected only if called on an object which is expiring.
void pass( input_file f ) && {
pass( std::move( f ) ); // dispatch to lvalue case
finalize();
}
Now the user almost never needs to worry about remembering to call finalize
, since most compiler objects are ultimately instantiated as temporaries.
Note, this sort of thing isn't particular to ref-qualified members. Any function can have separate overloads for t &
and t &&
. The way pass
is actually presently implemented uses perfect forwarding and then backtracks to determine the correct semantics:
template< typename compiler, typename arg >
void pass( compiler && c, arg a ) {
c.take_input( a );
if ( ! std::is_reference< compiler >::value ) {
c.finalize();
}
}
There are many ways to approach overloading. Actually, unqualified member functions are unusual in not caring about the category (lvalue or rvalue) of the object they are called on, and not passing that information into the function. Any function parameter besides the implicit this
must say something about the category of its argument.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With