Ref-qualifiers are frequently overlooked. Andreas Fertig reminds us why they are useful.
In this article, I discuss an often unknown feature: C++11’s ref-qualifiers. My book, Programming with C++20 [Fertig21], contains the example in Listing 1.
class Keeper {
std::vector<int> data{2, 3, 4};
public:
~Keeper() { std::cout << "dtor\n"; }
// Returns by reference
auto& items() { return data; }
};
// Returns by value
Keeper GetKeeper()
{
return {};
}
void Use()
{
// ① Use the result of GetKeeper and return
// over items
for(auto& item : GetKeeper().items()) {
std::cout << item << '\n';
}
}
|
Listing 1 |
What I have illustrated is that there is an issue with range-based for
-loops. In ①, we call GetKeeper().items()
in the head of the range-based for
-loop. By doing 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 [Josuttis21] 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 Listing 2.
class Keeper { std::vector<int> data{2, 3, 4}; public: ~Keeper() { std::cout << "dtor\n"; } // ② For lvalues auto& items() & { return data; } // ③ For rvalues, by value auto items() && { return data; } }; |
Listing 2 |
In ②, 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 ②, items
look like before, except for the &
. But we have an overload in ③, 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 safe.
Considering performance
From a performance point of view, you might see an unnecessary copy in ③. The compiler isn’t able to implicitly move the return value here. It needs a little help from us.
In Listing 3, in ④, you can see std::move
. Yes, I have told you in the past only rarely to use move
[Fertig22], but this is one of the few cases where moving actually helps, assuming that data is movable and that you need the performance.
class Keeper {
std::vector<int> data{2, 3, 4};
public:
~Keeper() { std::cout << "dtor\n"; }
auto& items() & { return data; }
// ④ For rvalues, by value with move
auto items() && { return std::move(data); }
};
|
Listing 3 |
Another option is to provide only the lvalue version of the function, removing the second items
function. Without this function, all calls from a temporary object to the remaining lvalue items
function result in a compile error. You have a design choice here.
Summary
Ref-qualifiers give us finer 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 safe for this programming style.
References
[Fertig21] Andreas Fertig (2021) Programming with C++20: Concepts, Coroutines, Ranges, and more, Fertig Publications, ISBN 978-3949323010
[Fertig22] Andreas Fertig (2022) ‘Why you should use std::move only rarely’, posted 1 February 2022 at: https://andreasfertig.blog/2022/02/why-you-should-use-stdmove-only-rarely/
[Josuttis21] Nicolai Josuttis, on Twitter: https://twitter.com/NicoJosuttis/status/1443267749854208000
[PR2012R2] Nicolai Josuttis, Victor Zverovich, Filipe Mulonde and Arthur O’Dwyer ‘Fix the range-based for loop R2’ available at https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2012r2.pdf
This article was published on Andreas Fertig’s blog on 5 July 2022 and is available at: https://andreasfertig.blog/2022/07/the-power-of-ref-qualifiers/
is a trainer and lecturer on C++11 to C++20, who presents at international conferences. Involved in the C++ standardization committee, he has published articles (for example, in iX) and several textbooks, most recently Programming with C++20. His tool – C++ Insights (https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs.