Logo

Blog


The power of ref-qualifiers

In today's post, I discuss an often unknown feature, C++11's ref-qualifiers.

My book, Programming with C++20, contains the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Keeper {  A 
  std::vector<int> data{2, 3, 4};

public:
  ~Keeper() { std::cout << "dtor\n"; }

  B Returns by reference
  auto& items() { return data; }
};

C Returns by value
Keeper GetKeeper()  
{
  return {};
}

void Use()
{
  D Use the result of GetKeeper and return over items
  for(auto& item : GetKeeper().items()) {
    std::cout << item << '\n';
  }
}

What I illustrated is that there is an issue with range-based for-loops. In D, we call GetKeeper().items() in the head of the range-based for-loop. By this, we create a dangling reference. The chain here is that GetKeeper returns a temporary object, Keeper. On that temporary object, we then call items. The issue now is that the value returned by items does not get lifetime extended. As items returns a reference to something stored inside Keeper, once the Keeper object goes out of scope, the thing items references does as well.

The issue here is that as a user of Keeper, spotting this error is hard. Nicolai Josuttis has tried to fix this issue for some time (see P2012R2). Sadly, a fix isn't that easy if we consider other parts of the language with similar issues as well.

Okay, a long bit of text totally without any reference to ref-qualifiers, right? Well, the fix in my book is to use C++20's range-based for-loop with an initializer. However, we have more options.

An obvious one is to let items return by value. That way, the state of the Keeper object doesn't matter. While this approach works, for other scenarios, it becomes suboptimal. We now get copies constantly, plus we lose the ability to modify items inside Keeper.

ref-qualifiers to the rescue

Now, this brings us to ref-qualifiers. They are often associated with move semantics, but we can use them without move. However, we will soon see why ref-qualifiers make the most sense with move semantics.

A version of Keeper with ref-qualifiers looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Keeper {
  std::vector<int> data{2, 3, 4};

public:
  ~Keeper() { std::cout << "dtor\n"; }

  A For lvalues
  auto& items() & { return data; }

  B For rvalues, by value
  auto items() && { return data; }
};

In A, you can see the ref-qualifiers, the & and && after the function declaration of items. The notation is that one ampersand implies lvalue-reference and two mean rvalue-reference. That is the same as for parameters or variables.

We have expressed now that in A, items look like before, except for the &. But we have an overload in B, which returns by value. That overload uses && meaning it is invoked on a temporary object. In our case, the ref-qualifiers help us make using items on a temporary object save.

Considering performance

From a performance point of view, you might see an unnecessary copy in B. The compiler isn't able to implicitly move the return value here. It needs a little help from us.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Keeper {
  std::vector<int> data{2, 3, 4};

public:
  ~Keeper() { std::cout << "dtor\n"; }

  auto& items() & { return data; }

  A For rvalues, by value with move
  auto items() && { return std::move(data); }
};

Above in A, you can see the std::move. Yes, I told you in the past to use move only rarely (Why you should use std::move only rarely), but this is one of the few cases where moving actually helps, assuming that data is movable and that you need the performance.

Another option is to provide only the lvalue version of the function, making all calls from a temporary object to items result in a compile error. You have a design choice here.

Summary

Ref-qualifiers give us more fine control over functions. Especially in cases like above, where the object contains moveable data providing the l- and rvalue overloads can lead to better performance -- no need to pay twice for a memory allocation.

We use more and more a functional programming style in C++. Consider applying ref-qualifiers to functions returning references to make them save for this programming style.

Andreas