2

By mistake, I had a & at the end of a prototype (see example below). Neither gcc nor Clang complains about it. I noticed that the symbol generated is not exactly the same.

Example:

class A
{
 public:
  void fn() &
  {
    return;
  }
};

int main()
{
  A a;
  a.fn();
  return 1;
}

Name of the symbol without &: _ZN1A2fnEv and with &: _ZNR1A2fnEv.

What does it mean ? Do I miss something ?

Thanks,

1 Answers1

8

The & at the end of the member function declaration is a ref qualifier. It applies to the object value on which the member function is called, and constrains that value's value category:

  • Functions without ref qualifiers can be called on any value.
  • Functions with & qualifier can only be called on lvalues.
  • Functions with && qualifier can only be called on rvalues.

The ref qualifier affects overload resolution, e.g. an overload on a mismatched instance value is not viable.

The standard library doesn't use ref qualifiers much (I can only think of std::optional), so let's make up our own example:

struct X {
  explicit X(int n) : s_(n, 'a') {}

  std::string s_;

  const char* f1() const    { return s_.c_str(); }
  const char* f2() const &  { return s_.c_str(); }
  const char* f3() const && { return s_.c_str(); }
};

Now consider the following calls:

int main() {
  X x(10);

  x.f1();      // OK
  X(20).f1();  // OK

  x.f2();      // OK
  X(20).f2();  // ill-formed

  x.f3();      // ill-formed
  X(20).f3();  // OK
}

The example also demonstrates why this feature may be useful: When a member function returns a reference to some internal part of the object itself, then it is important that that internal reference does not outlast the lifetime of the object. If your member function is unqualified, then you can very easily introduce lifetime bugs. For example:

const char* s = std::string("abcde").c_str();  // dangling pointer!

One way to improve such "internal state access" APIs is to create different return value (categories) for different ref-qualified overloads. To get back to std::optional, the engaged-access essentially boils down to this set of overloads:

struct MyOptional {
  T value_;  // assume engaged!

  T&  get() &  { return value_; }
  T&& get() && { return std::move(value_); }
};

That means that MyOptional::get returns an lvalue when invoked on an lvalue optional, and an rvalue (in fact an xvalue) when invoked on an rvalue. This means that, given MyOptional x;, the binding T& r = x.get(); is allowed, but T& r = MyOptional().get(); is not, and similarly T&& r = x.get(); is disallowed, but T&& r = std::move(x).get() is allowed.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084