Hacker News new | past | comments | ask | show | jobs | submit login
Lvalues, rvalues, glvalues, prvalues, xvalues, help (2018) (knatten.org)
246 points by signa11 on April 11, 2019 | hide | past | favorite | 92 comments



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.


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.


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


> 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?


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


> don't use move on an object that's doing RIAA to take and release a lock.

Absolutely. If the question is "what would I do in C++ today" then my answer is that I would use RAII subject to common-sense rules like this.

> Explicitly disable those actions (private copy constructor and operator, private move)

That's a decent enough solution within the context of C++ as an unchangeable thing, but I want people to look beyond that context. C++ has never been an unchangeable thing. Programmers shouldn't have to take such extra steps, so closely tied to how the language works that year, to get a conceptually simple result. That's how bugs creep in, and how even correct code becomes drudgery. We have no choice but to accept it now, but we shouldn't accept it permanently.


Automatic moves don't imply that you can't use RAII to define dynamic scopes. In fact it's the reverse- moves give RAII much more power and flexibility around scopes. The problem, instead, is that C++ has no way to determine whether later statements depend on moved-from variables.

To a first approximation, this could be solved using the same analysis that powers "use of uninitialized variable warnings." In the general case you need something more powerful, like the C++ Core Guidelines lifetime checker (https://herbsutter.com/2018/09/20/lifetime-profile-v1-0-post...). This means move operations end scopes just like `}`s do, preserving both RAII and the compiler's ability to help the programmer.


No, lack of good code analysis is not the problem. If the compiler was to only do a move in cases where it could prove variable independence then it would be operating under the as-if rule -- which it is already allowed to do -- and hence your code wouldn't behave any differently than if it hadn't done that. Automatic moves OTOH change the semantics of the code, which is kind of the point -- the compiler can't do an automatic move without permission unless the language specification says so, and unlike with the case of copy elision, it doesn't permit the compiler to change the semantics like this, as RAII has multiple use cases as I mentioned above that would break if this was permitted.


Yes, we're discussing an alternative design here. An automatic move should change the semantics of the program, rendering later uses invalid.

This does not break RAII- it merely changes the defaults. It should be copies that are marked in the program source, not moves- this is unfortunately not backwards compatible, but it's also not a problem for how RAII gets used.


Dynamic scopes are not available in C++. I suppose them as in languages like Emacs Lisp. I get the point, though.


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

This is already done in some places. Example:

    std::unique_ptr<int> get_int() {
        auto p = std::make_unique<int>(1);

        // `p` is an lvalue but treated as an rvalue in the return statement.
        // (This would not compile otherwise because `p` is not copyable.)
        return p;
    }


Yes RVO is one case where the compiler will do it automatically.

I think copy initialization of an object will have the same will apply copy elision as well to result in the same performance, but I'm not entirely sure.


That is the right approach IMO. What I'm basically saying is "more of this". It's clearly possible for C++ compilers to do this in many more cases. They already do most of the hard parts just to produce some of the error messages that they do. Why not use that knowledge more often to help the programmer instead of burdening them?


Because the compiler can't always know when moving a value is acceptable.


> It's clearly possible for C++ compilers to do this in many more cases. [...] Why not use that knowledge more often to help the programmer instead of burdening them?

Because it'd break code. I explained in another comment here: https://news.ycombinator.com/item?id=19634423


> Because it'd break code.

Then that's an indictment of the decisions that led to such code being written in the first place.


Only if you're quick to judge without understanding why such code is and will continue to be written.


You're pretty quick to assume I don't. Believe me, I understand. I just don't agree that it's a good idea to mix up object lifetimes and execution context by using destructors to "magically" release locks etc. It never was. The mistakes were made years ago.

I'm not quick to judge (in this case). I'm judging after careful consideration, because I know the difference between good and bad patterns. The ones being hasty are those who mistake their own comfort level with something (often because they know nothing else) for actual merit.


RAII is one of the core language feature of C++.

Replacing it with defer or GC will surely break existing programs.

It would make sense ONLY if you want to have a new language.


> It would make sense ONLY if you want to have a new language.

Are you seriously suggesting that C++ with defer is a new language? Unlike, say, C++ with lambdas? That's just silly.


C++ without deterministic destructors to manage lifetime would be a (fundamentally) different language.

Furthermore, also disagree with you that it would be a better language. C++ has many warts but object lifetime and RAII is amongst its strong suits (unarguably, I thought — yes, resource lifetime is complex, but it’s inherently so; C++ just makes the complexity explicit and handles it in a good way). Handling resource lifetime in languages with nondeterministic GCs can be such a pain that it has fundamental, detrimental impact on the architecture. Just look at .NET’s handling of `Dispose`. I’ve written a ton of .NET GUI code, and handling resource lifetime (in particular GDI+) is an absolute pain point, which is uniquely caused by the lack of deterministic object lifetime.


> C++ without deterministic destructors to manage lifetime would be a (fundamentally) different language.

The problem is that it's not usefully deterministic once you add in exceptions, shared pointers, move semantics, lambda captures, etc. Not to mention every perverse combination of those things. Yes, it's deterministic in the tautological sense that almost everything is deterministic given enough information, but I'm not sure that's enough even for the people who write the compilers. Even they have bugs related to misunderstanding this "deterministic" system. I certainly wouldn't want to make it less deterministic, and defer certainly doesn't make it so.

> resource lifetime is complex, but it’s inherently so

A certain amount of complexity is natural, a certain amount is spurious and self-inflicted. See above for some of the causes of that spurious complexity.

> C++ just makes the complexity explicit

Being explicit about memory management isn't a goal. C is even more explicit about these things. Does that make it a better language? Even in C++, new/delete is more explicit than most of the current idioms, and every book on modern C++ recommends against them. Explicit memory management should be a last resort, for the cases not handled cleanly by the language's other constructs, and those should be kept to a minimum. C++ has notably failed at that. Literally every other popular language except for C does better.

> Handling resource lifetime in languages with nondeterministic GCs can be such a pain

Do you really see no alternatives between dumping all memory-management complexity on the programmer and a full tracing GC? The authors of Objective C's automatic reference counting or Rust's borrow checker might take issue with that, as would the people who developed the memory-lifetime rules and infrastructure for all of the bigger older C codebases I mentioned in another comment. It is in fact possible for object lifetimes to be far more deterministic than in C++ as it exists today, and I for one think that would be a good thing.


> The problem is that it's not usefully deterministic once you add in exceptions, shared pointers, move semantics, lambda captures, etc.

No, it really is, even in the presence of the things you mentioned. That’s the whole point.

> Being explicit about memory management isn't a goal. […]

I get the impression that you’re confusing explicit and manual memory management. They’re not the same.

> Do you really see no alternatives between dumping all memory-management complexity on the programmer and a full tracing GC?

Of course I do, but the alternatives aren’t without their own problems. I’m curious how Rust will fare but Objective C’s ARC, while attractively simple, has performance implications, and then there’s the problem with cycles.


Rust has ARC and RC, which I gather are badly overused by refugees from other languages dependent on GC.

The better programs and libraries use them less. It remains to be seen whether this aesthetic will win out.


> The problem is that it's not usefully deterministic once you add in exceptions, shared pointers, move semantics, lambda captures, etc. Not to mention every perverse combination of those things.

It really is though, and I can't stress that enough. I feel you just throwing out keywords to make it sound more complex than it is.


> I can't stress that enough.

You can stress an untrue statement all you like. Shout it to the heavens. It will still be untrue.

Thought for you to consider: the behavior might seem predictable to you but that's not a relevant standard. A chess opening, a volleyball play, a snowboard run might all seem straightforward to me, but that doesn't mean they'd suit everyone. It doesn't mean they're the best. It just means I've invested my time in learning to do those things those ways. It's too easy to say everyone should have to memorize the same lists of rules, to retreat into "we don't care about the blubs" arrogance, ignoring the fact that it just doesn't have to be that way. It is in no way necessary, for any purpose, to make every single programmer in a language spend so much of their time looking over their shoulder to make sure the compiler is doing the right thing. It's a waste no matter how good those programmers are.

> I feel you just throwing out keywords

And I feel that you're just not even trying to understand their relevance because you've already decided on a conclusion. The lengths to which people in this thread go to rationalize the time they've already wasted is astonishing. People who use C++ should be the first to demand its improvement, but I guess not all humans are rational.


You might have picked the wrong hill to die on.

C++ destructors really are deterministic, even in the face of exceptions, lambdas, and moves: everything constructed gets destroyed. Modern wrinkles where the compiler is allowed to skip a construction and a destruction do not contradict that. You have to fool with heap memory or evoke UB to escape that law.


To be useful, determinism has to mean not only that something happens but that it happens at a predictable time. Otherwise you're into that tautological "everything is deterministic" territory, and you step into that even deeper when you acknowledge the "modern wrinkles". "Compiler is allowed" (but not required) is practically the definition of non-determinism.


"Compiler is allowed" not to destroy what it never constructed.

This is no different from the myriad other elisions modern compilers do -- and that CPU cores do.

Deterministically, destructors run exactly when the constructed object goes out of scope, after objects constructed later, before those constructed earlier. More determinism than that is something you will get from nobody.

You don't have to write any constructors at all. Write ordinary functions, and call them whenever you like. Been there, had enough of that for several lifetimes. Destructors are better.


The behavior is predictable to anyone versed in the language. C++ is a complex language to learn so that is inherently harder than most alternatives, but the concepts here are not.

I am first in line to acknowledge the flaws of C++ as well as sheering on more modern approaches. But I do find your criticisms shallow and a quite oddly chosen.


> The behavior is predictable to anyone versed in the language.

Ahh, there's that "don't care about the blubs" arrogance again. Never mind that your interlocutor is about 99.9% likely to be less of a blub than you are. Whether behavior is predictable given arbitrary amounts of information and effort is not the point. What matters is how much it distracts the programmer from the non-language-specific problem they're really trying to solve, and the answer for C++ remains way too damn much even when the programmer is highly skilled and well versed in the language.


You know you reek of it yourself.

C++ and even C has tons of subtle edges that are impossible to keep track of. You need to be experienced to have a fighting chance, I'm not defending that. But it is a fact of life and in a large part of that are relics from the past.

You can't just take a concept that is somewhat unique to C++ and proclaim that it is bad just because C++ has a lot of warts. Your criticism doesn't even register on the weirdness scale in my opinion. It works as intended, is easy to reason about and solves practical problems. Object lifetimes is something I almost always miss when I don't use C++. A garbage collector is hell to work with in comparison.

What is unfortunate is that we don't really have any alternatives. Rust shows promise but we've had to suffer through decades to even get to this point, which also implies that we have decades left. And that assumes that rust continues in the pace it has and preferably also that we get more alternatives to chose from.


Your proposed better pattern to follow when one needs a finally block in C++ is...?


I already mentioned "defer" as in Go or Zig. It's explicitly tied to scope exit, not object lifetime, so it avoids all of the problems inherent in confusing the two.


I didn't ask what language you would use. I asked what you would do in C++, given you're criticizing people for writing such code in the language. If you're put so much thought into this problem like you claim then surely you must have a better alternative in mind.


I don't care what people "would do" in C++ today, because my whole point is that C++ started down this bad path years ago. I'm not criticizing people for writing such code. I'm criticizing the standards-makers who made such hacks (seem) necessary. It's not about having put thought into it either. Lots of people had put plenty of thought into it when the various versions of C++ were standardized. This is about making the right choices from among the alternatives available, and that was not done.

Demanding a solution that is both applicable to C++ as it exists today and yet not in C++ today is demanding two contradictory things. It's demanding that the same thing both did and didn't happen. It's dishonest. You want a suggestion? Adopt "defer" for the next version of C++. It's the best we can do. We can't change the past, but we can learn from it if we don't get stuck trying to excuse past mistakes and attack those who point them out.


suppose you have a shared ptr and you pass it to a function with overloads for move and value. what should the compiler deduce? it can't tell if it's legal. it can't tell if it's illegal

I agree this is a nasty thing to rely on, but id rather be verbose and know what's happening than have more deduction guides/similar


If the compiler can't deduce what to do or whether it would be safe then sure, it should require the programmer to disambiguate. The keywords and underlying mechanisms should exist, just as with type casts and such. However, I see a lot of cases where what the intent is obvious and the result obviously safe. Often the compiler even manages to identify the one reasonable answer, but instead of applying it or at least presenting it to the user in readily applicable form it throws an error with the answer converted into standards-ese and surrounded with a dozen lines of irrelevant context. That's not helpful.

I've worked on a lot of large old C codebases, mostly kernels and filesystems. Every one has developed its own way to handle issues of object lifetime, ownership, etc. I've seen dozens of implementations of automatic reference counting, borrowing, weak references, and so on. Some of them have been widely and rightly regarded as awful and a pain to use, but none of them were as bad as the mess that C++ imposes on a much larger audience. I see a lot of my colleagues submit to Stockholm Syndrome and accept it as the proper way of the world, but I'm not inclined to follow their lead without registering my objections first.


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?

How do you even tell that it's legal to stick std::move around a variable? Here's a really simple example.

void test() { std::string s = ...; foo(s); bar(s); }

Can I change the last line to bar(std::move(s))? I can write a well-formed program that will misbehave, perhaps something like

char* global; void foo(const std::string& s) { global = s.data(); }

void bar(const std::string& s) { assert(global == s.data()); }

(Note that s.data() is not necessarily unchanged after std::move'ing the object, due to small-string optimization.)

This is contrived, but my point is that this is really tricky to do automatically.

Note also that when adding rvalue references the language committee had the additional constraint of not breaking existing programs. This makes things like automatic inference of temporariness even trickier.

The compiler does many other kinds of inference and automatic promotion/conversion, some of them far more difficult (and dangerous).

The C++ compiler does exceptionally few transformations that can change the behavior of a well-formed program. In fact, the only one that comes to mind is copy elision (aka RVO).


> The C++ compiler does exceptionally few transformations that can change the behavior of a well-formed program.

So all of those conversions between numeric, pointer, or string-ish types are just figments of my imagination? The rules for which constructor to use, which template or overload to apply, aren't a form of inference? "Auto" doesn't exist? Maybe there's some class of transformations and some definition of "well formed" for which your statement is true, but I can't imagine what those are.


Those are all things you asked to have. The topic was things you coded, but that the compiler did not generate code to do.

To be precise: by the object model, a constructor and destructor were supposed to be called. The compiler, by special dispensation, avoided calling them.

You may complain that the object model is whatever the standard says the compiler must do, in which case the statement would be vacuous: no transformations.


This is simply not acceptable because it would silently break too much code. Parameter passing in C++ used to mean copy, not move.

Perhaps a more important rule is that id-expressions should mean lvalues, not xvalues. Note that even expressions of rvalue references are not xvalues in such contexts. So, making something to-be-moved visually different from others is quite intentional.

Alternatively, to prevent use-after-move cases by additional syntaxes with typechecking rules (like Rust) can be a good idea, but it also does not work here. And C++ still lacks destructive move. (Note this has been considered at the very beginning of the design. Sadly it does not easily interact well with other features. See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n137...)


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.


I spent five years in the C++ mines, and spooky corners and edge cases and arcane rules like this made me happy to leave.

Working in a language where I can be concerned with the task and hand and not ownership issues is refreshing.


C++ is for sure extremely complicated, but I’d argue that a full understanding of value categories (glvalues, xrvalues, etc) isn’t necessary to be productive with the language.


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.


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.


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.


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


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?


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


Generally, yes, throw.

Sometimes an optional is better, such as when downstream already takes one.

Occasionally, checking whether the operation (and a bunch of others) would succeed is best, particularly in high-rel/high-av systems. There, you are probably also logging, first, what you are about to attempt.


Use a variant type like llvm::Expected.


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


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

Good advice. Corollary: don't use tools that make it hard to do simple things, or force you to live with the not-simple things others have done.


My available choices for what I do are C++ and, theoretically, Rust.

Rust has the advantage of fewer legacy choices thrust upon it, although it is rapidly building up its own legacy. It is thus far less expressive, so I cannot capture as much meaning in a library. It is less mature, limiting its practical usability. It has some complications C++ lacks, some of which are unavoidable even in little programs.

By the time Rust is mature, it (including its milieu) will be as complicated as C++ is today, with as many legacy boat anchors. But it will be as expressive and as practically useful, and I will have two choices of commensurate standing. For now, C++ is really all we have.

It all traces back to the destructor: the original innovation that makes the rest possible. Rust's Drop trait fills the identical role. Nothing else has it.

How do I deal with C++'s complexity? I stay the hell away from things that are too hard to understand. It's not hard. The most useful parts of the language are simple when used right.


RVO is guaranteed in C++17


To me, knowing C++, especially the "modern" dialect, is good for job security but like you I prefer plain old C for everything else.


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


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


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


I like that so much more!


Also, Rust does not have copy or move constructors/operators; every copy or move is a pure memcpy() (there's Clone for when you need it, but clone is always an explicit call). This simplifies the model a lot.


C++'s move constructor can throw exceptions. An example would be moving a linked list, and the allocation of dummy tail node can fail. In my opinion, the design choice to support recoverable allocation failure in C++ reasons for most other designs.


You can make allocation failure recoverable in different ways. For example, Rust’s (yet to be stabilized) allocator API returns a Result: https://doc.rust-lang.org/stable/std/alloc/trait.Alloc.html#...

It is true that choosing to make this recoverable is a big part of it; in Rust we decided to make standard library data structures treat allocation as unrecoverable, and this does simplify the API of using them significantly. However, I would argue that the complexity in C++ comes more from using exceptions for this purpose than from that choice. Rust’s panics being used for unrecoverable errors only helps simplify the overall model. In other words, the ease of use goes unrecoverable > recoverable through result > recoverable through exceptions, and the model complexity comes from the choice of using exceptions over return values. We do have a plan for introducing a way to make the data structures recoverable, but it’s waiting on several things.

This is, of course, only my opinion.


Exception is also a result of supporting recoverable allocation/construction failures, IMO. When you need to support a failable Vec::new, what will be the type of its Error? There are two choices in this case:

a) wrap underlying errors into something like VecNewError, which is tedious due to the prevalence of errors if every construction/allocation can fail, kindly similar to the CheckedException dilemma

b) type erasing it by Box it or dyn Error, and use ? operator to transparently pass errors to upper level, in which case the user need to downcast with match to extract information needed, and exception is essentially the syntax sugar for this case because exception acts like a ? operator after all failable function calls

a) is usually used in a language where errors are rare, and b) is preferred when the errors are common.


Vec::new doesn't allocate.

Vec::try_reserve, on the other hand, simply uses a common `CollectionAllocError`, which is not really (a) or (b).


Why would moving a list have to allocate a new tail node? A lot of the value in moves is passing data around without having to copy/duplicate/allocate.

The most useful aspect of having customisable move constructors like in C++ seems to be things like updating pointers in self-referential types, which hopefully can be written to never throw.


It's unfortunate but some C++ standard library implementations do require allocation in some moves (notably, IIRC, Microsoft's std::list). So we're stuck with it.


Huh, that is unfortunate!


Rust moves don't allocate, and yet can still support recoverable allocation failure.

The reason C++'s model is so much more complicated is because of backwards compatibility and nondestructive moves, not as a mechanism to support recoverable allocation failure.


lvalue/rvalue are old concepts and terms ('60s/'70s) and common to all C-family languages. The others are new to C++.



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

?


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`?


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


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.


great post!


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


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


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.


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.


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!


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.


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


They are not mutually exclusive. When a pointer has been set to point to an lvalue, dereferencing it yields that lvalue.


global variables are lvalues, for instance.


> rvalues do not and therefore cannot be addressed (with & or *)

Correction: prvalues cannot be addressed. There are also xvalues, which are both rvalues and lvalues i.e. they can be addressed but can also be moved from. In practice I think these are always lvalues that have been cast to an rvalue reference, usually using std::move().


This thread is recapitulating the article.


I used to think that. It's simple, right? Then i said so on the C++ Slack and got the hell beaten out of me. In modern C++, lvalue and rvalue simply don't mean what they do in other languages. Wild but true.


Lvalue and rvalue are inventions that serve a particular purpose in Algol-family languages. They are not physics.

These other value categories are further inventions for additional purposes. The only unfortunate bit is the insistence on one-letter abbreviations when discussing them.


C99 actually lets you make "pointer literals" (dont know the proper name) by putting an addressof operator in front of rvalue expressions. I cant offer a better explanation than the one you gave, though.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: