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
.
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.*this
of an rvalue
is an lvalue reference
and not an rvalue reference
?*this
. Is that related to this? Will any of the above change in C++14?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.
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&& .
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.
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.
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...
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.
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.
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