The Power of Ref-qualifiers

The Power of Ref-qualifiers

By Andreas Fertig

Overload, 30(171):7-8, October 2022


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/

Andreas Fertig 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.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.