4

Consider the following:

struct my_type {};

my_type make_my_type() { return my_type{}; }

void func(my_type&& arg) {}

int main()
{
    my_type&& ref = make_my_type();

    func(ref);
}

Needless to say, this code doesn't compile. I realise that I need to use std::move() in the second function call, but for the purposes of understanding I want to consider the code as it is.

Attempting to compile the above, Clang 3.5 tells me:

error: no matching function for call to 'func'

note: candidate function not viable: no known conversion from 'my_type' to 'my_type &&' for 1st argument void func(my_type&&) {}

While g++ 4.9 says something almost identical:

error: cannot bind 'my_type' lvalue to 'my_type&&'

note: initializing argument 1 of 'void func(my_type&&)'

These error messages have me rather confused, because while ref is certainly an lvalue, its type is still my_type&&... isn't it?

I'm trying to understand exactly what's going on here, so I'm wondering which (if any) of the following are true:

  • Since only rvalues can be bound to rvalue references, and ref is an lvalue, it cannot be bound to arg. The error messages from both Clang and g++ are misleading in claiming that ref is a (non-reference) my_type that "cannot be converted".

  • Because it is an lvalue, ref is treated for the purposes of overload resolution as a non-reference my_type, despite its actual type being my_type&&. The error messages from Clang and g++ are misleading because they are displaying the type as used internally for function matching, not the real type of ref.

  • In the body of main(), the type of ref is plain my_type, despite the fact I explicitly wrote my_type&&. So the error messages from the compilers are accurate, and it is my expectation that is wrong. This doesn't seem to be the case however, since

      static_assert(std::is_same<decltype(ref), my_type&&>::value, "");
    

    passes.

  • There is some other magic going on that I haven't considered.

Just to repeat, I know that the solution is to use std::move() to convert the rref back into an rvalue; I'm looking for an explanation of what's going on "behind the scenes".

Community
  • 1
  • 1
Tristan Brindle
  • 16,281
  • 4
  • 39
  • 82
  • 2
    `while ref is certainly an lvalue, its type is still my_type&&... isn't it?` Not really. For most intents and purposes, expressions of reference type don't exist. "**5/5** If an expression initially has the type “reference to T” (8.3.2, 8.5.3), the type is adjusted to T prior to any further analysis. The expression designates the object or function denoted by the reference, and the expression is an lvalue or an xvalue, depending on the expression." – Igor Tandetnik Oct 15 '14 at 05:19
  • I *think* [Nic's answer](http://stackoverflow.com/a/9552880/1322972) to a somewhat related question may provide a hint or two. – WhozCraig Oct 15 '14 at 05:19
  • @IgorTandetnik I've never quite got my head around the difference between the type of a *variable*, and the type of an *expression*... but perhaps that's key here? – Tristan Brindle Oct 15 '14 at 05:29
  • 3
    Well, what you pass as an argument to a function is an expression, as in `f(2+2)`. One possible form of an expression is *id-expression* (**5.1.1**) - just a single name (of a variable or named constant). In `func(ref)`, `ref` is an *id-expression* whose *unqualified-id* names a variable. The type of the variable is `my_type&&`; the expression is an lvalue of type `my_type`, thanks to the adjustment per **5/5**. – Igor Tandetnik Oct 15 '14 at 05:33
  • 1
    Your first bullet point is correct. The result of the *expression* `ref` is an lvalue. The compiler syntax is also correct, not only due to the above but also because there is no such thing as a "`my_type&&` lvalue". – user657267 Oct 15 '14 at 05:34
  • @IgorTandetnik Okay, I think I get it. A variable doesn't have a value category, an expression does. When treated as an expression (i.e. in a function call), the name of a reference variable (either type) loses its "reference-ness" -- so the type of the *variable* `ref` is `my_type&&`, but the type of the *expression* `ref` is `my_type`. Is that about right? – Tristan Brindle Oct 15 '14 at 05:40
  • @IgorTandetnik Ah, you edited your comment to say exactly that. Thanks, I think it's sunk in. If you'd like to write that up as an answer, I'll accept it. – Tristan Brindle Oct 15 '14 at 05:42
  • @user657267 Thanks, I think I understand what's going on now. The fact there's a difference between the type of an expression and the type of a variable is confusing, but that's the way it is... – Tristan Brindle Oct 15 '14 at 05:46
  • @TristanBrindle Indeed. Perhaps you see references as syntactically similar to pointers, I know I used to (the notation doesn't help, `*` vs `&`). They have similar purposes, but I find it helps if you remove this idea from your brain and treat them as completely separate. Pointers are values that point to other values; references are not "values" themselves (even applying the concept of a value when talking about references is nonsensical, hence the quotes), you can't dereference a reference (an oxymoron if there ever was one!). – user657267 Oct 15 '14 at 06:02
  • @user657267 It's not so much that -- my mental model of a pointer is just a number representing a memory address, which we can "look into" (dereference) to get access to the object, whereas a reference is something slightly different. But I guess I didn't realise how different! FWIW, I also get confused by the fact that the expression `(x)` is sometimes different to plain `x`... – Tristan Brindle Oct 15 '14 at 06:13
  • 3
    Use `expression_name()` (http://stackoverflow.com/a/20721887/576911) to output the type of the **expression** `ref`. Use `type_name()` (http://stackoverflow.com/questions/81870/print-variable-type-in-c/20170989#20170989) to output the **declared type** of `ref`. – Howard Hinnant Oct 15 '14 at 08:53
  • @HowardHinnant Fantastic! Thanks very much. Those two functions will be a great help for debugging in future -- `std::type_info::name()` doesn't really cut it. Any chance you could suggest something like that for the standard library in future? ;-) – Tristan Brindle Oct 15 '14 at 11:07

1 Answers1

0

Consider these three assignments:

my_type x = func_returning_my_type_byvalue();
my_type & y = func_returning_my_type_byvalue();
my_type && z = func_returning_my_type_byvalue();

The first - you have a local variable x and it's being initialized to the result of a function call (rvalue) so a move constructor/assignment can be used or the construction of x could be elided entirely (skipped over and x is constructed in-place by func_returning_my_type_byvalue when it generates its result).

Note that x is an lvalue - you can take its address, so therefore it is also a type of reference itself. Technically all variables that are not references, are references to themselves. In that respect lvalues are a binding site for assignments to and reads from known-storage-duration memory.

The second will not compile - you cannot assign a reference to a result (this way), you must use reference assignment syntax to alias an existing lvalue. It is perfectly fine to do this, however:

my_type & y = func_returning_my_type_byreference();
// `y` will never use constructors or destructors

This is why the third exists, when we need a reference to something we cannot create a reference to using the conventional syntax. Within something like func in the original question, the lifetime of arg is not immediately obvious. For example we can't do this without an explicit move:

void func( my_type && arg ) {
    my_type && save_arg = arg;
}

The reason this is not allowed is because arg is a reference to a value first and foremost. If the storage of arg's value (what it's referring to) were to be shorter than that of save_arg, then save_arg would call the destructor of that value - in effect capturing it. That is not the case here, save_arg will disappear first, so it makes no sense to transfer an lvalue into it that we can, after func, still refer to potentially!

Consider that even if you were to use std:move to force this to compile. The destructor will still not be called within func because you haven't created a new object, just a new reference, and then this reference is destroyed before the original object itself went out of scope.

For all intents and purposes arg behaves as if it's my_type&, as do any rvalue references. The trick is storage duration and the semantics of lifetime extension by reference passing. It's all regular references under the hood, there is no 'rvalue type'.

If it helps, recall the increment / decrement operators. There are two overloads that exist, not two operators. operator++(void) (pre) and operator++(int) (post). There is never an actual int being passed, it's just so the compiler has different signatures for different situations / contexts / agreements about value treatment. This is sort of the same deal with references.

If rvalue and lvalue references are both always referred to like an lvalue, what's the difference?

In a word: object lifetime.

An lvalue reference must always be assigned to using something with longer storage duration, something that is already constructed. That way there is no need to call constructors or destructors for the scope of the lvalue reference variable, because by definition we are given a ready object and forget about it before it's due to be destroyed.

it's also relevant that objects are implicitly destroyed in the reverse order they're defined:

int a; // created first, destroyed last
int b; // created second, destroyed 2nd-last
int & c = b; // fine, `c` goes out of scope before `b` per above
int && d = std::move(a); // fine, `a` outlives `d`, same situation as `c`

If we assigned to an rvalue reference, something that is an lvalue reference, the same rule applies - the lvalue must by definition have longer storage, so we don't need to call constructors or destructors for c or even d. You can't trick the compiler with std::move on this because it knows the scope of the object being moved - d is unambiguously shorter-duration than the reference it's being given, we're just forcing the compiler to use the rvalue type check / context and that's all we've achieved.

The difference is with non-lvalue references - things like expressions where there can be references to them but these references are definitely short-lived, perhaps shorter than the duration of a local variable. Hint Hint.

When we assign the result of a function call or an expression to an rvalue reference, we are creating a reference to a temporary object that otherwise could not be referred to. Due to this, we are in effect forcing in-place construction of a variable from the result of an expression. This is a variation on copy/move elision where the compiler has no choice but to elide the temporary to in-place construction:

int a = 2, b = 3; // lvalues
int && temp = a + b; // temp is constructed in-place using the result of operator+(int,int)

The case with func

It boils down to an lvalue assignment - references as function arguments refer to objects that may exist for longer than a function call, and as such are lvalues even when the argument type is an rvalue reference.

The two cases are:

func( std::move( variable ) ); // case 1
func( my_type() + my_type() ); // case 2

func is not allowed to guess which situation we will use it in ahead of time (sans optimizations). If we didn't allow case 1, then there would be a legitimate reason to consider an rvalue reference parameter as having less storage duration than the function call, but that would also make no sense because either the object is always cleaned up inside func or always outside of it, and having "unknown" storage duration at compile time is not satisfactory.

The compiler has no choice but to assume the worst, that case 1 might happen eventually, in which case we must make guarantees to the storage duration of arg as being longer than the call to func in the general case. As consequence of this - that arg would be considered to exist for longer than the call to func some of the time, and that func's generated code must work in both cases - arg's allowable usage and assumed storage duration meet the requirements of my_type& and not my_type&&.

Xeren Narcy
  • 875
  • 5
  • 15