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, it becomes suboptimal in other scenarios. 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 |
|
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 |
|
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 are using a functional programming style in C++ more and more. Consider applying ref-qualifiers to functions returning references to make them save for this programming style.
Andreas