Ref-qualifiers

In this post I want to describe a small language feature added in C++11 that, although essential for full value semantics support, is often neglected in tutorials and in compiler implementations.

The problem

Do you know Fernando Cacciola’s Boost.Optional library? It is used for representing ‘nullable’ or ‘optional’ objects that may have not been assigned a value. A C++14-compatible variant thereof is now being added to Library Fundamentals TS. One of the goals is that optional<T> works smoothly with moveable but non-copyable types; and even for copyable types, it should not make any copies unless absolutely necessary. This is a very basic usage scenario:

optional<T> read(); // may return T or not-a-T

if (optional<T> v = read()) // has value?
  use(*v);                  // get the value

In line 3 we move-construct an optional<T> which internally move constructs a T — no copying. But the library offers more ways to access the potential value:

T t = read().value();     // (1)
T u = read().value_or(0); // (2)
T v = *read();            // (3)

Function value() either returns a contained value (if one is present) or throws an exception. Function value_or() either returns the contained value or returns the provided default. operator* either returns the contained value or renders an undefined behaviour, if no value is present. It is “unsafe” but the fastest alternative at the same point. It is to be used when we are convinced by other means that the value is there.

Let’s pick line 1. Is any copying of T involved? Obviously, we do not need to have two copies of optional<T> at any time; but we have a “syntactical” problem in reflecting this. What is the signature of function(s) value? In C++03 the best we can do is this:

template <typename T>
class optional
{
  // ...
  T&       value();
  T const& value() const;
};

This is a fairly familiar construct. “If the container is const, give me const access; if the container is mutable, give me a mutable access. But in both cases we are returning an lvalue reference. Whichever overload is picked, it will trigger the copy construction of the T object subsequently. There is no way from the member function to tell that it is called for an rvalue. The only thing we could do to prevent copying is this:

T t = std::move(read().value());

But this is just repulsive, and likely forgotten.

The solution

Even if one knows rvalue references and how to apply them for function arguments, one may not know how to apply them to the ‘implied object’ (*this). Had value() been a non-member function, we would know what to do:

T &      value(optional<T> &      this_);
T &&     value(optional<T> &&     this_);
T const& value(optional<T> const& this_);

But what to do for member functions? This is solved by the c++11 feature that we can call “ref-qualifiers for member functions”. The original proposal that describes it is N2439. It enables us to write:

template <typename T>
class optional
{
  // ...
  T&       value() &;
  T&&      value() &&;
  T const& value() const&;
};

The syntax may be confusing at first, but it becomes logical when we imagine that each (non-static) member function can be thought of as a free function with one additional special parameter in front: the implied *this object. Then these references apply to this implied parameter.

The *this pointer

But this way of reasoning with the implied object cannot be pushed too far. For instance, if we applied it to a good old const member function, it would mean that we are passing the implied object by value, which is not true.

The root of the problem here boils down to the fact that this is a pointer rather than a reference. There is a historical justification for this choice. At the point OO support was added to C++, the language did not have references yet, so this had to be a pointer. And later, when references have been added… you know… backwards compatibility. But this worked fine until rvalue references were introduced. At that time you could say that the const/mutable qualifiers on member functions said how type T — which this points to — is qualified:

struct Type
{
  void mem();
  void mem() const;
};

// equivalent to:

void mem(Type * this_);
void mem(Type const* this_);

But in C++11 we need to be able to say that a member function is invoked on a temporary. For references, it is easily denoted by rvalue references, but there is no such thing for pointers. Therefore in C++ we have these two incompatible ways of looking at the implied object: sometimes as reference and sometimes as pointer. This incompatibility is also visible at C++ syntax level:

struct Type
{
  void mem() const;
  void mem() &;     // ERROR
};

You have to decide whether you are using the ref or non-ref qualifiers and you cannot mix them within one overload set. But keyword this remains a pointer in either case.

Yes, I know what you are saying at this point. The backwards compatibility is often a PITA, but it is also one of C++’s strengths.

Const-ness is not a function’s property

I would like to make one remark now that we are discussing the const and ref-qualifiers on member functions. Note that unlike other ‘modifiers’ like static, constexpr, noexcept, [[noreturn]], these two do not constrain the function, but only the implied *this parameter. You can freely call non-const member function from a const function. The only thing you cannot do is to call a non-const function on *this object. See this example:

struct Type
{
  int _i = 0;
  void modify(Type & t) const
  {
    t._i = 1;
  }
};

int main()
{
  Type t;
  t.modify(t);
  assert (t._i == 1);
}

An object changed its own value from within a const-qualified member function! Everything is const-correct!

Ref-qualifiers in your compiler

Does your compiler support ref-qualifiers? The first proposal for rvalue references was submitted on 2002 (N1377), whereas the first proposal for member function ref-qualifiers was submitted 3 years later, on 2005 (N1784). This lag is also reflected in compiler support of these features. The following table summarizes the state as I know it:

Compiler rvalue refs ref-qualifiers source
GCC 4.3 4.8.1 here
Clang 2.9 2.9 here
Intel 12.0 14.0 here
Visual C++ VC10 Nov 2013 CTP here

BTW, you can find the reference implementation of std::experimental::optional library, which exploits ref-qualifiers, here.

This entry was posted in programming and tagged , , . Bookmark the permalink.

6 Responses to Ref-qualifiers

  1. Great article! (as always)

  2. Mike Tryhorn says:

    Thank you for giving this feature a clear explanation!

  3. Nir Friedman says:

    IMHO the

    “`
    T && value(optional && this_);
    “`
    overload continues to be dubious, though widely used in the standard library. It leads very easily to dangling references when you do things like

    “`
    const auto& x = make_an_optional().value();
    “`

    A rvalue ref qualified overload should not be handing out references to its internal members, except in the particular case where the reference it’s handing out is precisely a sub-object, in which case the lifetime extension mechanism applies (although this is quite a subtle point and is bugged in even fairly recent versions of gcc).

    • Nir Friedman says:

      Forgot to add (and can’t edit here), it should return by value. In common use cases because of move elision there is zero performance difference. It’s only once you start chaining these things that you see differences, and even then it’s only moves which tend to be cheaps.

  4. Pingback: Does std::optional forwards rvalueness when contained object functions are called? – Windows Questions

  5. Pingback: C++23’s Deducing this: what it is, why it is, how to use it – IT GEEK NEWS

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.