
Lvalues, rvalues, glvalues, prvalues, xvalues, help (2018) - signa11
https://blog.knatten.org/2018/03/09/lvalues-rvalues-glvalues-prvalues-xvalues-help/
======
notacoward
Excellent explanation. Too bad it's necessary. && and std::move are the
biggest warts in C++ (that's saying a lot), representing the need for
programmers to be constantly aware of the less-than-obvious ways that a
compiler might put an expression in one category or another. It's the
programmer helping the compiler, instead of the other way around as it should
be. In general, if I need an rvalue and it's _legal_ to convert the lvalue I
have into an rvalue, the compiler should do it _automatically_. Having to make
it explicit should be the exception, not the rule. The compiler does many
other kinds of inference and automatic promotion/conversion, some of them far
more difficult (and dangerous). Why not this one? I suspect the reason has
less to do with the actual ergonomics of producing correct programs than with
the the interests and self-image of those creating the standards.

~~~
mehrdadn
It's not what what you're asking is unreasonable in concept, but that it goes
against how RAII is used in C++; it'd make more sense in a new language.
Specifically the issue is that despite its name, RAII isn't merely used for
resource acquisition, but also to define dynamic scopes. That means destroying
an object too early (which can happen if an object is automatically moved) can
cause a subsequent statement that doesn't syntactically depend on the variable
holding that object to see the wrong program state. So if your compiler moved
objects automatically, that'd break code that uses RAII as a dynamic scoping
mechanism.

That said though, it might be cool to have an [[attribute]] for classes (maybe
[[automove]]? or [[resource]]? or [[functional]] or [[dataflow]]??) that lets
you declare that that a class's construction and destruction (or maybe all
members... ) can be assumed to have no side-effects for clients to depend on,
or something like that... if they can work it out that'd be cool. Ideally it'd
mean that an automatic variable of that type can be destroyed following its
last dependency in any given scope.

~~~
notacoward
> it goes against how RAII is used in C++

That's an excellent point. To be honest, I think it's a problem with how RAII
is used, conflating execution scope and object lifetime in a way that makes
"move" a bit of a mess. That's why I prefer explicit "defer" like some
languages have. Nonetheless, it's a common and useful enough idiom that
breaking it is not something to be taken lightly. Maybe it's not really a
solvable problem, given the paths that C++ took years ago, but I'd say "too
bad it's necessary" is even more applicable if that's the case.

~~~
AnimalMuppet
> I think it's a problem with how RAII is used, conflating execution scope and
> object lifetime in a way that makes "move" a bit of a mess.

It seems to me that there's a subset that is just fine. Use RIAA to, for
example, take a lock and automatically release it when you leave a scope.
That's not a problem, in itself. And go ahead and use move semantics, too.
That's also fine.

 _But don 't use move on an object that's doing RIAA to take and release a
lock._ That, in my view, is not much of a restriction, because if you think
about it, that's not really a reasonable thing to want to do. That RIAA lock-
taker is not the kind of object that you want to move, or even copy.
Explicitly disable those actions (private copy constructor and operator,
private move), and you should be good.

Or have I missed something?

~~~
mehrdadn
> if you think about it, that's not really a reasonable thing to want to do.
> That RIAA lock-taker is not the kind of object that you want to move, or
> even copy.

It actually is (at least re: moving). Tying lock acquisition to object
lifetime is a Good Thing (TM), and if you wanted to extend or shrink the
lifetime of that lock you should be able to do that by moving the RAII object,
the same as how you could do it with a shared_ptr or whatever.

------
meuk
Articles like this make me feel very smug about my decision never to go down
the rabbithole of learning C++.

I feel perfectly comfortable with using pointers (and my intuition about them)
in C.

~~~
chubot
FWIW I mostly use “out params” as in C because I think moves incur a lot of
complexity, as well explained in this article. It’s also more lines of code to
write. Some people may quibble with it but I think it’s still a fine style of
writing C++, and in fact most C++ you’ll see is written like that, since this
is a new-ish feature.

~~~
strictfp
Agreed. RVO is not guaranteed, and move is complex. Smart pointers have
overhead. Out params are for the most part a sensible choice in C++. Sadly.
I'm honestly shocked that this problem hasn't been addressed in a better way.

~~~
ncmncm
Out-parameters are a terrible choice in C++. If it matters whether RVO is
guaranteed, you are probably making something more complicated than it needs
to be.

Value semantics give you sensible behavior. Smart pointers should not appear
in user-visible interfaces.

It is easy to tie yourself in knots by doing the complicated thing. The
solution is to do the simple thing.

~~~
leetcrew
> Out-parameters are a terrible choice in C++

why? I personally love functions like `EResult function(const T1& inParam1,
T2& outParam2)`. the function signature makes it obvious what the function
does (or should do, at least) and it's easy to avoid accidental copying.

~~~
ncmncm
Code like that is the bane of my existence. Give me `T2 function(T1)` every
time. Besides being overwhelmingly clearer, I can use it in just one line. The
other needs at least 3, more commonly 5, lines packed around it, so less than
20% of your code actually does anything useful toward the problem it is
supposed to solve. Five times longer means you have five times the bugs.

Besides being horrible style, operating on references cripples the optimizer.
"Avoid accidental copying"? What good is that if it's slower than if you did
the copy?

~~~
signa11
serious question: would you throw an exception if T2 cannot be created ? or
use a variant data-type ? or something else altogether?

~~~
bigcheesegs
Use a variant type like llvm::Expected.

~~~
signa11
yup, that's what i thought. i generally tend to use a construct simiar to
'error_or' though. that wrapped within a macro (^^) makes the whole invokation
quite bearable

------
bagol
Is this concept of lvalues, rvalues, glvalues, prvalues and xvalues only
exists in C++?

~~~
Sharlin
Few other languages have move semantics. Rust does, but has a quite different
value model overall.

~~~
steveklabnik
Our equivalent to lvalue/rvalue is “place expression” and “value expression”
[https://doc.rust-
lang.org/reference/expressions.html#place-e...](https://doc.rust-
lang.org/reference/expressions.html#place-expressions-and-value-expressions)

~~~
nixpulvis
I like that so much more!

------
Jeaye
Similarly, the value category cheatsheet: [https://github.com/jeaye/value-
category-cheatsheet/blob/mast...](https://github.com/jeaye/value-category-
cheatsheet/blob/master/value-category-cheatsheet.pdf) and more info on
xvalues:
[https://blog.jeaye.com/2017/03/19/xvalues/](https://blog.jeaye.com/2017/03/19/xvalues/)

------
contravariant
Maybe this is just naive / wishful thinking, but wouldn't it have made _way_
more sense to go with either:

glvalues, lvalues, grvalues, rvalues, xvalues

or

lvalues, plvalues, rvalues, prvalues, xvalues

?

------
rocqua
Is it totally exactly true that a `glvalue` is either an `xvalue` or a
`lvalue` and nothing else?

Similarly, is a `rvalue` either a `xvalue` or a `prvalue`?

~~~
dbaupp
Yes, section 8.2.1 "Value Category" (page 80) of the C++17 standard[0] defines
the terms as:

(1.1) - A glvalue is an expression whose evaluation determines the identity of
an object, bit-field, or function.

(1.2) - A prvalue is an expression whose evaluation initializes an object or a
bit-field, or computes the value of the operand of an operator, as specified
by the context in which it appears.

(1.3) - An xvalue is a glvalue that denotes an object or bit-field whose
resources can be reused (usually because it is near the end of its lifetime).

(1.4) - An lvalue is a glvalue that is not an xvalue.

(1.5) - An rvalue is a prvalue or an xvalue.

It also has a diagram summarising it (equivalent to the one in the article,
but the article's is clearer IMO):

    
    
             expression
              /      \
          glvalue    rvalue
           /    \    /    \
       lvalue   xvalue    prvalue
    

Where every expression is in one of the three value categories in the bottom
row.

[0]: available free as a late draft: [http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2017/n471...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf)

~~~
rocqua
Thanks, I actually recall seeing that diagram.

I agree that the article is clearer. Because I didn't get it when I saw that
diagram, but I do get it after reading the article.

------
agallego
great post!

------
somedudeatwork
lvalues are locatable, they represent an object that occupies some
identifiable location in memory (has address). rvalues do not and therefore
cannot be addressed (with & or *)

I think of lvalues as things on the stack and rvalues as things stored in
registers

[https://eli.thegreenplace.net/2011/12/15/understanding-
lvalu...](https://eli.thegreenplace.net/2011/12/15/understanding-lvalues-and-
rvalues-in-c-and-c/)

~~~
dbaupp
The article starts by explaining lvalues and rvalues more precisely than that,
and breaks them up into 3 additional value categories in modern C++ (the ones
in the title).

(Also, lvalues aren't tied to the stack: they can be anywhere in memory, like
heap or static.)

~~~
leetcrew
how can you have an lvalue stored in the heap? isn't the lvalue itself a
pointer in the stack? obviously I see how it can have static storage.

~~~
dbaupp
If foo is an lvalue, then I believe (for example)

    
    
       *(foo[0].bar->baz)
    

is also an lvalue, and those pointer dereferences can go anywhere. Those
values occupy a location in memory, as you said, even if it isn't on the
stack.

~~~
leetcrew
oh okay, understood. frankly I don't have an airtight understanding of the c++
standards and I would have considered your expression to be a dereference
operation _on_ an lvalue, but not an lvalue itself. ty for the explanation!

~~~
simiones
I'm sure there are some complex exceptions, but as a starter, you can think of
lvalues as "anything you can assign to" (the l basically stands for left, as
in, "can appear on the left of an assignment"). So, if you have something like
`foo(val) = 10`, then `foo(val)` is an lvalue.

~~~
mannykannot
One of the complications is that if you take an l-value and const-qualify it,
it is still an l-value (but not a modifiable l-value.)

[https://www.geeksforgeeks.org/lvalue-and-rvalue-in-c-
languag...](https://www.geeksforgeeks.org/lvalue-and-rvalue-in-c-language/)

