IMHO, the original suggestion by Dietmar Kühl (providing overloads for &
and &&
ref-qualifiers) is superior than Simple's one (providing it only for &
).
The original idea is:
class G {
public:
// other members
G& operator=(G) & { /*...*/ return *this; }
G operator=(G) && { /*...*/ return std::move(*this); }
};
and Simple has suggested to remove the second overload. Both solutions invalidate this line
G& g = G() = G();
(as wanted) but if the second overload is removed, then these lines also fail to compile:
const G& g1 = G() = G();
G&& g2 = G() = G();
and I see no reason why they shouldn't (there's no lifetime issue as explained in Yakk's post).
I can see only one situation where Simple's suggestion is preferable: when G
doesn't have an accessible copy/move constructor. Since most types for which the copy/move assignment operator is accessible also have an accessible copy/move constructor, this situation is quite rare.
Both overloads take the argument by value and there are good reasons for that if G
has an accessible copy/move constructor. Suppose for now that G
does not have one. In this case the operators should take the argument by const G&
.
Unfortunately the second overload (which, as it is, returns by value) should not return a reference (of any type) to *this
because the expression to which *this
binds to is an rvalue and thus, it's likely to be a temporary whose lifetime is about to expiry. (Recall that forbidding this from happening was one of the OP's motivation.)
In this case, you should remove the second overload (as per Simple's suggestion) otherwise the class doesn't compile (unless the second overload is a template that's never instantiated). Alternatively, we can keep the second overload and define it as delete
d. (But why bother since the existence of the overload for &
alone is already enough?)
A peripheral point.
What should be the definition of operator =
for &&
? (We assume again that G
has an accessible copy/move constructor.)
As Dietmar Kühl has pointed out and Yakk has explored, the code of the both overloads should be very similar and, in this case, it's better to implement the one for &&
in terms of the one for &
. Since the performance of a move is expected to be no worse than a copy (and since RVO doesn't apply when returning *this
) we should return std::move(*this)
. In summary, a possible one-line definition is:
G operator =(G o) && { return std::move(*this = std::move(o)); }
This is good enough if only G
can be assigned to another G
or if G
has (non-explicit) converting constructors. Otherwise, you should instead consider giving G
a (template) forwarding copy/move assignment operator taking an universal reference:
template <typename T>
G operator =(T&& o) && { return std::move(*this = std::forward<T>(o)); }
Although this is not a lot of boiler plate code it's still an annoyance if we have to do that for many classes. To decrease the amount of boiler plate code we can define a macro:
#define ASSIGNMENT_FOR_RVALUE(type)
template <typename T>
type operator =(T&& b) && { return std::move(*this = std::forward<T>(b)); }
Then inside G
's definition one adds ASSIGNMENT_FOR_RVALUE(G)
.
(Notice that the relevant type appears only as the return type. In C++14 it can be automatically deduced by the compiler and thus, G
and type
in the last two code snippets can be replaced by auto
. It follows that the macro can become an object-like macro instead of a function-like macro.)
Another way of reducing the amount of boiler plate code is defining a CRTP base class that implements operator =
for &&
:
template <typename Derived>
struct assignment_for_rvalue {
template <typename T>
Derived operator =(T&& o) && {
return std::move(static_cast<Derived&>(*this) = std::forward<T>(o));
}
};
The boiler plate becomes the inheritance and the using declaration as shown below:
class G : public assignment_for_rvalue<G> {
public:
// other members, possibly including assignment operator overloads for `&`
// but taking arguments of different types and/or value category.
G& operator=(G) & { /*...*/ return *this; }
using assignment_for_rvalue::operator =;
};
Recall that, for some types and contrarily to using ASSIGNMENT_FOR_RVALUE
, inheriting from assignment_for_rvalue
might have some unwanted consequences on the class layout.