const rvalue references – useful use-cases

Rvalue references are generally used to signal a value that will no longer be used by the caller. Could be a temporary value who’s lifetime will expire when the call returns, or an lvalue which is wrapped with a std::moveto signal it will no longer be used any further. If this isn’t old news, you can review a short description here or with your favorite search engine.

When we bind an rvalue with a function call, the usual form is:


void f(A&& a)
{
   A a1;
   a1.data = a.data;
  
   // rvalue reference a is “moved” from. 
   a.data = nullptr;

   // Note: you'd likely use a swap, 
   // but this is clearer.
}

This is pretty standard stuff. Instead of making a possibly expensive copy of what member variable data contained, we take over the pointer. What this implies is that the parameter a, is modified. The very notion of rvlaue references is based on the idea that we no longer need what was passed as an argumen to the function once the call is returned, and we can extract what we need from it, and leave it in valid, yet unspecified state. But, what if the parameter was set as const in the signature?

void f(const A&& a)

What would this be??? The parameter is const, meaning we can’t extract and “steal” the underlying data in parameter a like we did above. This implies were back to the slow lane of copying if we need to copy and not just access the parameter. Binding wise, a const rvalue can already bind to this overload:

void f(const A& a)

Which allows us to do the same as a const rvalue bind. ie. Being const means the parameter can be accessed, but if we need to make a copy, we’re in the slow-lane. Even if we have a temporary value being passed to a const lvalue parameter, the life-time will be extended. Hence, no advantage to the const r-value reference parameter form.

Which brings us to: Is this form just an oddity of the language? Similar to protected inheritance (see item 32 Effective C++)? Turns out there are in fact a few corner cases where const r-value references can be of use: when explictly setting it as a deleting form. From the standard, search for const T&& yields 4 templates with the deleted form.

// From C++ 11:
template<class T> void ref(const T&&) = delete;
template<class T> void cref(const T&&) = delete;

// From C++17:
template<class T> 
void as_const(const T&&) = delete;
template<class T> 
const T* addressof(const T&&) = delete;

In all cases above, the end result in the above template functions is to prevent binding to const rvalues. Which makes sense for what these template functions want to accomplish. If would be possible to disallow binding of rvalues to the function templates using a combo of type traits. Specifically by combining is_const and is_rvalue_reference, but would definitely be more verbose.

The case for using const rvalue reference, and not =delete as above, is implying a case where we don’t support the operation for lvalues, and also, we can’t actually move thus need to make a copy. I’ve not encountered this form.

The final use-case is by using reference qualifers, specifically with const rvalue reference. An example of that is std::optional<> in C++17, where its value() function is declared as this:

constexpr const T& value() const&;
constexpr T& value() &;
constexpr T&& value() &&;

// note the constness of rvalue reference 
// for both return type and reference qualifier.
constexpr const T&& value() const&&;

Here’s an example of how that would come into play:

// Note constness
const std::optional<int>
crv_func()
{
    return std::optional<int> (42);
}

// non-const
std::optional<int>
rv_func()
{
    return std::optional<int> (5);
}

// ok, uses const rvalue ref
const int&& cval = crv_func().value(); 

// ok uses non-const rvalue ref
int&& val = rv_func().value(); 

Conclusion: From the 3 use-cases above, there are indeed way where const rvalue refences can be useful and worth knowing they do come in handy.

Leave a Reply

Your email address will not be published. Required fields are marked *