29

I can't remember which talk it was, but recently I watched some talks from CppCon 2017 and there someone mentioned as some kind of side-note, that the only true way of overloading operator= would be in the following fashion:

class test {
public:
    test& operator=(const test&) &;
};

He explicitly emphasized the trailing & but didn't say what it does.

So what does it do?

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
Martin B.
  • 1,567
  • 14
  • 26
  • It's a means of overloading the function based on the *lvalue/rvalue type of the object on which the member function is called.* So, 2 objects (instantiations) of the same class, calling the same function (method) name, can actually be calling 2 different function implementations, because this function can be overloaded based on whether the object calling the method is a **const lvalue** (`const &` after the func params), regular **lvalue** (`&`), **const rvalue** (`const &&`), or regular **rvalue** (`&&`). Source: that's my summary of this answer: https://stackoverflow.com/a/47003980/4561887. – Gabriel Staples May 29 '20 at 21:33
  • 1
    @GabrielStaples To be even more specific w.r.t. different [value categories](https://en.cppreference.com/w/cpp/language/value_category), const and non-const prvalues as well as xvalues (as xvalues bind to rvalue references) will both favour rvalue ref-qualifier overloads, if they are present. If we remove the rvalue ref-qualifier overloads but leave the explicit lvalue ref-qualifier overloads, these rvalue categories will all favour the `const &` overload, as an rvalue may be used to initialize a const lvalue reference. – dfrib Jun 09 '20 at 11:19

2 Answers2

38

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

  • (4.1) — “lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier

  • (4.2) — “rvalue reference to cv X” for functions declared with the && ref-qualifier

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.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • 5
    And...this is why C++ is completely nuts. Great answer, by the way. – Gabriel Staples May 15 '20 at 19:34
  • 1
    I spend far far far more time learning about mere *syntax* in C++ than I EVER did in C. In C, I learned the syntax and struggled with the architecture, then I was just productive. In C++ I never seem to have learned all the syntax, so the open-ended question of "when will I truly just be productive?" always seems to be an ever-changing answer which is just out-of-reach. – Gabriel Staples May 15 '20 at 19:38
  • 1
    @GabrielStaples I can totally agree to the ”ever a C++ novice” feeling, particularly as many parts of the language are evolving rapidly. I dabbled with Swift for a few years (a very nice language), but for some reason I’ve stuck C++ even given the challenges that come with it. Maybe I actually find the ever-learning situation quite interesting. But who knows. I think one can come quite far w.r.t. productivity just using a subset of the language (and knowing that subset well), but there can indeed be quite a threshold to reach even this point. – dfrib Jun 08 '20 at 21:32
  • Thanks for the empathy and for relating with me. I see you're from Sweden. You don't happen to work for these guys, do you? https://candelaspeedboat.com/. Pretty awesome boat. – Gabriel Staples Jun 08 '20 at 23:08
  • 2
    I had not heard of Candela, it looks really impressive. They are located on the west coast of Sweden (near Stockholm) though, whereas I'm located on the east coast (Gothenburg), working with transportation on land rather than by sea; safety critical C++ development in the automotive industry (AD/ADAS software in realtime embedded-ish/POSIX environments). This usually means a strict subset of C++, which may be why I turn to answering StackOverflow questions and working hobby projects for using a larger portion of the language, under less restrictions :) – dfrib Jun 09 '20 at 11:02
  • (East/west correction: Gothenborg is west coast, Stockholm is east coast :-) ) – dfrib Feb 08 '21 at 12:01
6

As per http://en.cppreference.com/w/cpp/language/member_functions the & following your member function declaration is lvalue ref-qualifier.

In other words, it requires this to be an l-value (the implicit object parameter has type lvalue reference to cv-qualified X). There is also &&, which requires this to be an r-value.

To copy from documentation (const-, volatile-, and ref-qualified member functions):

#include <iostream>
struct S {
  void f() & { std::cout << "lvalue\n"; }
  void f() &&{ std::cout << "rvalue\n"; }
};

int main(){
  S s;
  s.f();            // prints "lvalue"
  std::move(s).f(); // prints "rvalue"
  S().f();          // prints "rvalue"
}
Adam Kotwasinski
  • 4,377
  • 3
  • 17
  • 40