Does an rvalue keep its "status" when a const reference parameter binds to it?

57 Views Asked by At

Let T be an arbitrary type. Consider a function that takes a const [lvalue] reference:

void f(const T &obj);

Suppose that this function internally makes a call to another function, which has an rvalue reference overload:

void g(T &&obj);

If we pass an rvalue to f, will the rvalue reference overload of g be called, or will it fail to do so since it has been "converted"/bound to a const lvalue reference?

Similarly, if f called a function that takes an instance of T by value,

void h(T obj);

and T has a move constructor, (i.e. T(T &&);), will the move constructor be called, or will the copy constructor be called?

In conclusion, if we wanted to ensure that, when calling f on an rvalue, the rvalue reference is passed around keeping its rvalue "status", would we necessarily have to provide an rvalue reference overload for f?

2

There are 2 best solutions below

0
On BEST ANSWER

When you use the name of a reference as an expression, that expression is always an lvalue. No matter if it's a lvalue reference or an rvalue reference. It also doesn't matter if the reference was initialized with an lvalue or an rvalue.

int &&x = 1;
f(x); // Here `x` is lvalue.

So in void f(const T &obj) {...}, obj is always an lvalue, regardless of what you pass as an argument.

Also note that value category is determined at compile time. Since f is not a template, value category of every expression in it can't depend on arguments you pass.

Thus:

If we pass an rvalue to f, will the rvalue reference overload of g be called

No.

if f called a function that takes an instance of T by value, void h(T obj); and T has a move constructor, (i.e. T(T &&);), will the move constructor be called

No.

In conclusion, if we wanted to ensure that, when calling f on an rvalue, the rvalue reference is passed around keeping its rvalue "status", would we necessarily have to provide an rvalue reference overload for f?

Providing an overload is one option. Note that in this case you have to explicitly call std::move in the rvalue overload.

Another option is using forwarding references, as Nicol Bolas suggests:

template <typename T> void f(T &&t)
{
    g(std::forward<T>(t));
}

Here, std::forward essentially acts as a 'conditional move'. It moves t if an rvalue was passed to it, and does nothing otherwise.

0
On

Value categories are applied to expressions, not objects. obj in f is an lvalue expression, and will therefore be treated as such. Note that obj in g is also an lvalue expression; if the expression is a name for an object, then it's an lvalue.

The ability you're talking about is exactly why forwarding references exist: so that the copy/move aspects of an argument expression's value category can be preserved through function calls. f would have to become a template of the form template<typename T> void f(T&& t);, and you would have to use std::forward when passing it to g.