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
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
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
In A, you can see the ref-qualifiers, the
&& 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.
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
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.
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.