Operator Overload Which Permits Capturing with Rvalue But Not Assigning To

Operator overload which permits capturing with rvalue but not assigning to

Have the assignment operator be callable on lvalues only:

class C
{
// ...

public:

C& operator=(const C&) & = default;
};

Note the single ampersand after the closing parenthesis. It prevents assigning to rvalues.

Prevent assigning to rvalue

If you don't want your operator=() to be invoked on temporaries (rvalues), then qualify it lvalue as:

Test& operator =(const Test&) & {return *this;}
//^^^ note this

Now the following

f() = t;

will give compilation error.

C++ Is a special rvalue-variant of an overloaded operator necessary?

This signature:

inline X operator+(X lhs, const X& rhs)

allows both rvalues and lvalues as the left-hand side of the operation. lvalues would just be copied into lhs, xvalues would be moved into lhs, and prvalues would be initialized directly into lhs.

The difference between taking lhs by value and taking lhs by const& materializes when we chain multiple + operations. Let's just make a table:

+====================+==============+=================+
| | X const& lhs | X lhs |
+--------------------+--------------+-----------------+
| X sum = a+b; | 1 copy | 1 copy, 1 move |
| X sum = X{}+b; | 1 copy | 1 move |
| X sum = a+b+c; | 2 copies | 1 copy, 2 moves |
| X sum = X{}+b+c; | 2 copies | 2 moves |
| X sum = a+b+c+d; | 3 copies | 1 copy, 3 moves |
| X sum = X{}+b+c+d; | 3 copies | 3 moves |
+====================+==============+=================+

Taking the first argument by const& scales in the number of copies. Each operation is one copy. Taking the first argument by value scales in the number of copies. Each operation is just one move but for the first argument being a lvalue means an additional copy (or an additional move for xvalues).

If your type isn't cheap to move - for those cases where moving and copying are equivalent - then you want to take the first argument by const& since it is at least as good as the other case and there's no reason to fuss.

But if it's cheaper to move, you actually probably want both overloads:

X operator+(X const& lhs, X const& rhs) {
X tmp(lhs);
tmp += rhs;
return tmp;
}

X operator+(X&& lhs, X const& rhs) {
lhs += rhs;
return std::move(lhs);
}

This will use moves instead of copies for all the intermediate temporary objects, but will save you one move on the first one. Unfortunately, the best solution is also the most verbose.

Why does this C++ snippet assigning a value to an rvalue compile?

a*b = c; calls the assignment operator on the Rational returned by a * b. The assignment operator generated is the same as if the following were defined:

Rational& Rational::operator=(const Rational&) = default;

There is no reason why this shouldn't be callable on a temporary Rational. You can also do:

Rational{1,2} = c;

If you wanted to force the assignment operator to be callable only on lvalues, you could declare it like this, with a & qualifier at the end:

Rational& operator=(const Rational&) &;

Why can I use assignment operator on begin() even if it is an rvalue?

The reason is historical. In the initial days of the language, there was simply no way for user code to express that a type's copy-assignment operator should only work on l-values. This was only true for user-defined types of course; for in-built types assignment to an r-value has always been prohibited.

int{} = 42; // error

Consequently, for all types in the standard library, copy-assignment just "works" on r-values. I don't believe this ever does anything useful, so it's almost certainly a bug if you write this, but it does compile.

std::string{} = "hello"s; // ok, oops

The same is true for the iterator type returned from v.begin().

From C++11, the ability to express this was added in the language. So now one can write a more sensible type like this:

struct S
{
S& operator=(S const &) && = delete;
// ... etc
};

and now assignment to r-values is prohibited.

S{} = S{}; // error, as it should be

One could argue that all standard library types should be updated to do the sensible thing. This might require a fair amount of rewording, as well as break existing code, so this might not be changed.

Are temporary C++ objects lvalues?

Objects are not lvalues or rvalues. The words lvalue and rvalue are expression categories. They categorize expressions, not objects.


For the line:

returnsA() = anotherA;

since the operands of = are class types, an overloaded operator function is searched for. operator= is special in that if the user does not explicitly overload it, there is a default overload generated. That overload is a member function with signature:

A& A::operator=(A const&);

and the call returnsA() = anotherA; is transformed to returnsA().operator=(anotherA);, this is the basic mechanics of member operator overloading.

The expression returnsA() has category prvalue. However it is perfectly fine to call member functions on prvalue expressions, so there is no problem here.


If you want to dissuade your class from accepting this sort of assignment, see here. And for why the default assignment operator doesn't work that way, see here.

so what is the type of this ? Why is this not a lvalue?

For some class X, this has the type X* this;, but you're not allowed to assign to it, so even though it doesn't actually have the type X *const this, it acts almost like it was as far as preventing assignment goes. Officially, it's a prvalue, which is the same category as something like an integer literal, so trying to assign to it is roughly equivalent to trying to assign a different value to 'a' or 10.

Note that in early C++, this was an lvalue -- assigning to this was allowed -- you did that to handle the memory allocation for an object, vaguely similar to overloading new and delete for the class (which wasn't supported yet at that time).



Related Topics



Leave a reply



Submit