It's part of a feature allowing C++11 non-static member functions to differentiate between whether they are being called on an lvalues or rvalues.
In the above case, the copy assignment operator being defaulted here can only be called on lvalues. This uses the rules for lvalue and rvalue reference bindings that are well established; this just establishes them for this
.
In the above case, the copy assignment operator is defaulted only if the object being copied into can bind to a non-const lvalue reference. So this is fine:
C c{};
c = C{};
This is not:
C{} = c;
The temporary here cannot bind to an lvalue reference, and thus the copy assignment operator cannot be called. And since this declaration will prevent the creation of the usual copy assignment operator, this syntax effectively prevents copy-assignment (or move-assignment) to temporaries. In order to restore that, you would need to add a &&
version:
C& operator=(const C&) && = default;
C& operator=(C&&) && = default;
Ref-qualifiers - introduced in C++11
Ref-qualifiers is not C++17 feature (looking at the tag of the question), but was a feature introduced in C++11.
struct Foo
{
void bar() const & { std::cout << "const lvalue Foo\n"; }
void bar() & { std::cout << "lvalue Foo\n"; }
void bar() const && { std::cout << "const rvalue Foo\n"; }
void bar() && { std::cout << "rvalue Foo\n"; }
};
const Foo&& getFoo() { return std::move(Foo()); }
int main()
{
const Foo c_foo;
Foo foo;
c_foo.bar(); // const lvalue Foo
foo.bar(); // lvalue Foo
getFoo().bar(); // [prvalue] const rvalue Foo
Foo().bar(); // [prvalue] rvalue Foo
// xvalues bind to rvalue references, and overload resolution
// favours selecting the rvalue ref-qualifier overloads.
std::move(c_foo).bar(); // [xvalue] const rvalue Foo
std::move(foo).bar(); // [xvalue] rvalue Foo
}
Note that an rvalue may be used to initialize a const lvalue reference (and in so expanding the lifetime of the object identified by the rvalue), meaning that if we remove the rvalue ref-qualifier overloads from the example above, then the rvalue value categories in the example will all favour the remaining const &
overload:
struct Foo
{
void bar() const & { std::cout << "const lvalue Foo\n"; }
void bar() & { std::cout << "lvalue Foo\n"; }
};
const Foo&& getFoo() { return std::move(Foo()); }
int main()
{
const Foo c_foo;
Foo foo;
// For all rvalue value categories overload resolution
// now selects the 'const &' overload, as an rvalue may
// be used to initialize a const lvalue reference.
c_foo.bar(); // const lvalue Foo
foo.bar(); // lvalue Foo
getFoo().bar(); // const lvalue Foo
Foo().bar(); // const lvalue Foo
std::move(c_foo).bar(); // const lvalue Foo
std::move(foo).bar(); // const lvalue Foo
}
See e.g. the following blog post for for a brief introduction:
rvalues cannot invoke non-const
&
overloads
To possibly explain the intent of your recollected quote from the CppCon talk,
"... that the only true way of overloading operator=
..."
we visit [over.match.funcs]/1, /4 & /5 [emphasis mine]:
/1 The subclauses of [over.match.funcs] describe the set of candidate functions and the argument list submitted to overload
resolution in each context in which overload resolution is used. ...
/4 For non-static member functions, the type of the implicit object parameter is
where X
is the class of which the function is a member and cv is the
cv-qualification on the member function declaration. ...
/5 ... For non-static member functions declared without a ref-qualifier, an additional rule applies:
- (5.1) — 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. [ Note: The fact that such an argument is
an rvalue does not affect the ranking of implicit conversion
sequences. — end note ]
From /5 above, the following overload (where the explicit &
ref-qualifier has been omitted)
struct test
{
test& operator=(const test&) { return *this }
}
allows assigning values to r-values, e.g.
int main()
{
test t1;
t1 = test(); // assign to l-value
test() = t1; // assign to r-value
}
However, if we explicitly declare the overload with the &
ref-qualifier, [over.match.funcs]/5.1 does not apply, and as long we do not supply an overload declared with the &&
ref-qualifier, r-value assignment will not be allowed.
struct test
{
test& operator=(const test&) & { return *this; }
};
int main()
{
test t1;
t1 = test(); // assign to l-value
test() = t1; // error [clang]: error: no viable overloaded '='
}
I won't place any opinion as to whether explicitly including the &
ref-qualifier when declaring custom assignment operator overloads is "the only true way of overload operator=
", but would I dare to speculate, then I would guess that the intent behind such a statement is the exclusion of to-r-value assignment.
As a properly designed assignment operator should arguably never be const
(const T& operator=(const T&) const &
would not make much sense), and as an rvalue may not be used to initialize a non-const lvalue reference, a set of overloads for operator=
for a given type T
that contain only T& operator=(const T&) &
will never proviade a viable overload that can be invoked from a T
object identified to be of an rvalue value category.
Best Answer
It means the member will be invoked when the object is an lvalue reference.
Without a ref-qualifier, the function can always be invoked, regardless of the value category of the expression through which you're invoking it:
Live demo (thanks Praetorian)