Search code examples
c++move-semanticsrvalue-referencepass-by-const-reference

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


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?


Solution

  • 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.