c++c++14xvaluevalue-categoriesprvalue

xvalues vs prvalues: what does identity property add


I'm sorry for the broadness of the question, it's just that all these details are tightly interconnected..

I've been trying to understand the difference between specifically two value categories - xvalues and prvalues, but still I'm confused.

Anyway, the mental model I tried to develop for myself for the notion of 'identity' is that the expression that has it should be guaranteed to reside in the actual program's data memory.

Like for this reason string literals are lvalues, they're guaranteed to reside in memory for the entire program run, while number literals are prvalues and could e.g. hypothetically be stored in straight asm.

The same seems to apply to std::move from prvalue literal, i.e. when calling fun(1) we would get only the parameter lvalue in the callee frame, but when calling fun(std::move(1)) the xvalue 'kind' of glvalue must be kept in the caller frame.

However this mental model doesn't work at least with temporary objects, which, as I understand, should always be created in the actual memory (e.g. if a rvalue-ref-taking func is called like fun(MyClass()) with a prvalue argument). So I guess this mental model is wrong.

So what would be the correct way to think about the 'identity' property of xvalues? I've read that with identity I can compare addresses but if I could compare addresses of 2 MyClass().members (xvalue according to the cppreference), let's say by passing them by rvalue refs into some comparison function, then I don't understand why I can't do the same with 2 MyClass()s (prvalue)?

One more source that's connected to this is the answer here: What are move semantics?

Note that even though std::move(a) is an rvalue, its evaluation does not create a temporary object. This conundrum forced the committee to introduce a third value category. Something that can be bound to an rvalue reference, even though it is not an rvalue in the traditional sense, is called an xvalue (eXpiring value).

But this seems to have nothing to do with 'can compare addresses' and a) I don't see how this is different from the 'traditional sense' of the rvalue; b) I don't understand why such a reason would require a new value category in the language (well, OK, this allows to provide dynamic typing for objects in OO sense, but xvalues don't only refer to objects).


Solution

  • I personally have another mental model which doesn't deal directly with identity and memory and whatnot.

    prvalue comes from "pure rvalue" while xvalue comes from "expiring value" and it is this information I use in my mental model:

    Pure rvalue refers to an object that is a temporary in the "pure sense": an expression for which the compiler can tell with absolute certainty that its evaluation is an object that is a temporary that has just been created and that is immediately expiring (unless we intervene to prolong it's lifetime by reference binding). The object was created during the evaluation of the expression and it will die according to the rules of the "mother expression".

    By contrast, an expiring value is an expression that evaluates to a reference to an object that is promised to expire soon. That is it gives you a promise that you can do whatever you want to this object because it will be destroyed next anyway. But you don't know when this object was created, or when it is supposed to be destroyed. You just know that you "intercepted" it as it is just about to die.

    In practice:

    struct X;
    auto foo() -> X;
    
    X x = foo();
          ^~~~~
    

    in this example evaluating foo() will result in a prvalue. Just by looking at this expression you know that this object was created as part of the return of foo and will be destroyed at the end of this full expression. Because you know all of these things you can prolong its lifetime:

    const X& rx = foo();
    

    now the object returned by foo has its lifetime prolonged to the lifetime of rx

    auto bar() -> X&&
    
    X x = bar();
          ^~~~
    

    In this example evaluating bar() will result in an xvalue. bar promises you that it is giving you an object that is about to expire, but you don't know when this object was created. It can be created way before the call to bar (as a temporary or not) and then bar gives you an rvalue reference to it. The advantage is you know you can do whatever you want with it because it won't be used afterwards (e.g. you can move from it). But you don't know when this object is supposed to be destroyed. As such you cannot extend its lifetime - because you don't know what its original lifetime is in the first place:

    const X& rx = bar();
    

    this won't extend the lifetime.