Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to return an rvalue reference to this

The code below results in Undefined Behaviour. Be sure to read ALL the answers for completeness.


When chaining an object via the operator<< I want to preserve the lvalue-ness / rvalue-ness of the object:

class Avenger {
  public:
    Avenger& operator<<(int) & {
      return *this;
    }
    Avenger&& operator<<(int) && {
      return *this; // compiler error cannot bind lvalue to rvalue
      return std::move(*this);
    }
};
void doJustice(const Avenger &) {};
void doJustice(Avenger &&) {};

int main() {
  Avenger a;
  doJustice(a << 24); // parameter should be Avenger&
  doJustice(Avenger{} << 24); // parameter should be Avenger&&

  return 0;
}

I cannot simply return *this which implies that the type of *this of an rvalue object is still an lvalue reference. I would have expected to be an rvalue reference.

  • Is it correct / recommended to return std::move(*this) on an member overloaded for the && qualifier, or should other method be used? I know that std::move is just a cast, so I think it’s ok, I just want to double check.
  • What is the reason/explanation that *this of an rvalue is an lvalue reference and not an rvalue reference?
  • I remember seeing in C++14 something about move semantics of *this. Is that related to this? Will any of the above change in C++14?
like image 653
bolov Avatar asked May 26 '14 16:05

bolov


People also ask

Can you return an rvalue?

Returning a value from a function will turn that value into an rvalue. Once you call return on an object, the name of the object does not exist anymore (it goes out of scope), so it becomes an rvalue. Similarly, calling a function will give back an rvalue. The return value of a function has no name.

How do you use rvalue reference?

If the function argument is an rvalue, the compiler deduces the argument to be an rvalue reference. For example, assume you pass an rvalue reference to an object of type X to a template function that takes type T&& as its parameter. Template argument deduction deduces T to be X , so the parameter has type X&& .

Is an rvalue reference an lvalue?

An lvalue is an expression that yields an object reference, such as a variable name, an array subscript reference, a dereferenced pointer, or a function call that returns a reference. An lvalue always has a defined region of storage, so you can take its address. An rvalue is an expression that is not an lvalue.

Can we take address of rvalue?

rvalue — The expression that refers to a disposable temporary object so they can't be manipulated at the place they are created and are soon to be destroyed. An address can not be taken of rvalues. An rvalue has no name as its a temporary value.


3 Answers

The type of this depends on the cv-qualifier of the member function: Avenger* or const Avenger*

But not on its ref-qualifier. The ref-qualifier is used only to determine the function to be called.

Thus, the type of *this is Avenger& or const Avenger&, no matter if you use the && or not. The difference is that the overload with && will be used then the called object is a r-value, while the & will not.

Note that rvalue-ness is a property of the expression, not the object. For example:

void foo(Avenger &x)
{
    foo(x); //recursive call
}
void foo(Avenger &&x)
{
    foo(x); //calls foo(Avenger &)!
}

That is, although in the second foo(), x is defined as an r-value reference, any use of the expression x is still an l-value. The same is true for *this.

So, if you want to move out the object, return std::move(*this) is The Right Way.

Could things have been different had this been defined as a reference value instead of as a pointer? I'm not sure, but I think that considering *this as an r-value could lead to some insane situations...

I didn't hear of anything changing about this in C++14, but I may be mistaken...

like image 97
rodrigo Avatar answered Sep 25 '22 08:09

rodrigo


std::move is perhaps better called rvalue_cast.

But it is not called that. Despite its name, it is nothing but an rvalue cast: std::move does not move.

All named values are lvalues, as are all pointer dereferences, so using std::move or std::forward (aka conditional rvalue cast) to turn a named value that is an rvalue reference at point of declaration (or other reasons) into an rvalue at a particular point is kosher.

Note, however, that you rarely want to return an rvalue reference. If your type is cheap to move, you usually want to return a literal. Doing so uses the same std::move in the method body, but now it actually triggers moving into the return value. And now if you capture the return value in a reference (say auto&& foo = expression;), reference lifetime extension works properly. About the only good time to return an rvalue reference is in an rvalue cast: which sort of makes the fact that move is an rvalue cast somewhat academic.

like image 36
Yakk - Adam Nevraumont Avatar answered Sep 22 '22 08:09

Yakk - Adam Nevraumont


This answer is in response to bolov's comment to me under his answer to his question.

#include <iostream>

class Avenger
{
    bool constructed_ = true;

  public:
    Avenger() = default;

    ~Avenger()
    {
        constructed_ = false;
    }

    Avenger(Avenger const&) = default;
    Avenger& operator=(Avenger const&) = default;
    Avenger(Avenger&&) = default;
    Avenger& operator=(Avenger&&) = default;

    Avenger& operator<<(int) &
    {
      return *this;
    }

    Avenger&& operator<<(int) &&
    {
      return std::move(*this);
    }

    bool alive() const {return constructed_;}
};

void
doJustice(const Avenger& a)
{
    std::cout << "doJustice(const Avenger& a): " << a.alive() << '\n';
};

void
doJustice(Avenger&& a)
{
    std::cout << "doJustice(Avenger&& a): " << a.alive() << '\n';
};

int main()
{
  Avenger a;
  doJustice(a << 24); // parameter should be Avenger&
  doJustice(Avenger{} << 24); // <--- this one
  // Avenger&& dangling = Avenger{} << 24;
  // doJustice(std::move(dangling));
}

This will portably output:

doJustice(const Avenger& a): 1
doJustice(Avenger&& a): 1

What the above output demonstrates is that a temporary Avenger object will not be destructed until the sequence point demarcated by the ';' just before the comment "// <--- this one" above.

I've removed all undefined behavior from this program. This is a fully conforming and portable program.

like image 23
Howard Hinnant Avatar answered Sep 25 '22 08:09

Howard Hinnant