24

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.

Community
  • 1
  • 1
Ralph Tandetzky
  • 22,780
  • 11
  • 73
  • 120

3 Answers3

21

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
    }
}
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
Andrew Tomazos
  • 66,139
  • 40
  • 186
  • 319
  • Also consider members which don't make sense to call on rvalues, like assignment operator. – Puppy Jul 08 '13 at 10:39
  • @DeadMG: Yes, an rvalue (prvalue or xvalue) will not viably bind to `void f(T&)`, so the use cases there are the same as `T::f() &` w.r.t. the implicit object parameter. My main point is to notice the orthogonal relationship between a normal function parameter and the implicit object parameter - so a general understanding of value categories and overload selection as they apply to a normal function parameter can be repurposed as-is to the member function implicit object parameter (with only minor differences not worth mentioning). – Andrew Tomazos Jul 08 '13 at 10:52
  • FWIW, I wouldnt even bother with the moving. Just add to itself and move afterwards. – R. Martinho Fernandes Jul 08 '13 at 11:03
  • @R.MartinhoFernandes: It depends what the move constructor and `operator +=` actually do, but yeah in some cases it may be more efficient. Changed, thanks. – Andrew Tomazos Jul 08 '13 at 11:12
  • Shouldn't the `operator+(...) &&` return `C&&` in order to avoid unnecessary temporaries? – Ralph Tandetzky Jul 08 '13 at 11:41
  • @RalphTandetzky: This would risk the user binding `*this` to a reference which will not extend its lifetime. for example `const C& c = C() + C(); c.foo();`. `c` has been destroyed before the call of foo. – Andrew Tomazos Jul 08 '13 at 14:42
  • Urgh. Sick! I've not thought about that dirty corner of the language. You're right about that. – Ralph Tandetzky Jul 08 '13 at 14:54
5

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.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • "For non-static member functions declared without a ref-qualifier, an additional rule applies: even if the implicit object parameter is not const-qualified, an rvalue can be bound to the parameter as long as in all other respects the argument can be converted to the type of the implicit object parameter." So `Foo::mutate()` with no ref qualifier is viable against rvalue this.... – Andrew Tomazos Jul 08 '13 at 15:09
  • Further with respect to ranking: "S1 and S2 are reference bindings (8.5.3) and neither refers to an implicit object parameter of a non-static member function __declared without a ref-qualifier__, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference.". So using an rvalue will be ambiguous between `mutate() no-ref-qualifier` and `mutate()&&`. Was this your intention? – Andrew Tomazos Jul 08 '13 at 15:09
  • ie I think you end up with the effect you want, but the diagnostic may be non-ideal (error ambiguous overload). – Andrew Tomazos Jul 08 '13 at 15:12
  • No, it's not my intention, I simply missed a `&` on one of the overloads, fixed – Jonathan Wakely Jul 08 '13 at 17:51
  • Ok, in that case I think `void mutate()&& = delete;` is unnecessary as an rvalue won't bind to `void mutate()&` anyway. – Andrew Tomazos Jul 09 '13 at 14:47
  • Am I remembering wrong, or was there not a time when C++ compilers (at least some?) refused to call a non-const method of a temporary object? So this device is a way of getting that safety back, while still allowing such a call when it makes sense (e.g. when the temp object is a 'view' of a persistent object's data, and the method is not const because it changes the viewed data). – greggo Aug 25 '14 at 18:32
1

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.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421