1

Consider the following code: (compile)

void f(int&&) {}

template<class T>
void s(T&& value)
{
    using rv_ref = decay_t<T>&&;

    static_assert( is_same_v<decltype(value), rv_ref> );

    f(static_cast<rv_ref>(value)); // ok, but we've checked `value` is already of this type
    //f(value); // error: no matching function
}

int main()
{
    s(9);
}

What makes me flinch is the fact that even though the type of value is int&&, when it's being used as a value [and not an expression(as with decltype)], it suddenly becomes an l-value.

How is this explained to make sense?

Hrisip
  • 900
  • 4
  • 13
  • 1
    Anything that has a name is an lvalue – Jesper Juhl Feb 01 '20 at 17:41
  • I think the `value` here is a universal reference ([forwarding reference](https://en.cppreference.com/w/cpp/language/reference)). – Zheng Qu Feb 01 '20 at 18:07
  • @JesperJuhl, then it would be rational for `value` to have type `int&` [and not `int&&`]. And everything would still work as expected (right?) . – Hrisip Feb 01 '20 at 18:45

2 Answers2

1

You write this:

even though the type of value is int&&, when it's being used as a value [and not an expression(as with decltype)], it suddenly becomes an l-value.

One high-level way to think about lvalues and rvalues is that lvalues have names and can be assigned to. value obviously has a name, so it shouldn't be surprising that it's an lvalue.

One way to think of std::move() or your static cast is that it not only casts its argument to the correct type, but it also produces an rvalue expression.

Thinking about your two functions s and f:

  • Presumably, your function s takes an rvalue reference because you want to operate on rvalues, e.g. steal their resources with a move operation.
  • In such a function, you might want to call functions on value that either (i) steal its resources, or (ii) treat it as an lvalue and do not steal its resources.
  • The language lets you call std::move() for case (i), to tell f that f may steal resources.
  • The language lets you pass value to functions without std::move(), as an lvalue, for case (ii), so you can call functions without worrying about that. This might be desirable if you want s to steal resources later on.

This question is pretty similar: Rvalue Reference is Treated as an Lvalue?

NicholasM
  • 4,557
  • 1
  • 20
  • 47
  • Ok, I see. I've never thought of type deduction as depending on the expression [and not the type of the expression]. I'll see if I can ingest it. Many thanks. – Hrisip Feb 01 '20 at 18:03
0

The name of a variable used as an expression is always an lvalue, even if the variable's type is an rvalue reference. (Though decltype has some additional rules, not just looking at the expression's value category.) This is an intentional choice of the C++ language, because otherwise it would be too easy to accidentally move from a variable.

For example:

void debug_str_val(std::string var_name, std::string value);

void MyClass::set_name(const std::string& name)
{
    debug_str_val("name", name);
    m_name = name;
}

void MyClass::set_name(std::string&& name)
{
    debug_str_val("name", name);  // *
    m_name = std::move(name);
}

On the line marked with a *, name is treated as an lvalue, so debug_str_val gets a copy of the value. (It maybe could use const std::string& parameters instead, but it doesn't.) If name were treated as an rvalue, the debug_str_val would be moved from, and then an unpredictable value would be assigned to m_name.

Once an object has a name, it could be used more than once, even if that name is an rvalue reference. So C++ uses it as an lvalue by default and lets the programmer say when they want it to be used as an rvalue, using std::move, or sometimes std::forward, or an explicit cast to rvalue reference type.

aschepler
  • 70,891
  • 9
  • 107
  • 161