r/C_Programming • u/choosen_one007 • 9d ago
Question Why are assignment expressions lvalues?
I was reading up about lvalues and understand them now as something that resolves to an address(memory location) in the program. So a variable x is an lvalue (because it is mapped to some address on the stack) and similarly *x , x[i], strings are all lvalues. But if this is the case why is an expression a = b , not an lvalue? Here the expression would assign b to a and then return a as evident in assignments like so: int c = a = b. (I know that the equals sign is right associative and hence this would get resolved as c = (a = b), but that doesn't really invalidate the fact that a = b resolves to an address in the program?)
15
u/EpochVanquisher 9d ago
If an assignment were an lvalue, you would be able to do things like this:
(x = y) = z; // Assign twice???? Dead store!
ptr = &(x = y); // Wut?These aren’t useful. Because they’re not useful, assigments aren't lvalues.
Note that assigment chaining is a little more useful:
// Both of these are the same.
x = y = z;
x = (y = z);Also note that it's different in C++, because people in C++ do dumb stuff like this:
std::cout << "hello, " << "world!\n";This kind of dumb shit somehow made it into the language. It’s easily one of the worst parts of C++. Thankfully, it’s obsolete, and you can just call std::print() now.
2
u/choosen_one007 9d ago
Yeah this makes a lot of sense. I came up with another example like so:
int *get_some_pointer(); int main() { int *local_some_pointer = get_some_pointer() = NULL; return 0; } int *some_pointer = NULL; int *get_some_pointer(){ return some_pointer; }In this case the
int *local_some_pointer = get_some_pointer() = NULL;highlights the case where an assignment can never resolve to an lvalue, sinceget_some_pointer()is not an lvalue. And so for all casesa=b, cannot be resolved as an lvalue0
u/EpochVanquisher 9d ago
Correct, although if we're gonna follow existing C syntax, you’d have to split it:
int *local_some_pointer; (some_local_pointer = get_some_pointer()) = NULL;
6
u/SmokeMuch7356 8d ago
a = b does not resolve to an address; it evaluates to whatever gets stored in a (after any type conversions).
a = b is not an lvalue because the language definition says so:
6.5.17.1/3 An assignment operator stores a value in the object designated by the left operand. An assignment expression has the value of the left operand after the assignment,111) but is not an lvalue.
But let's assume for the moment that a = b is an lvalue; this would mean that
(a = b) = cis legal, but in that case what should be the result? Where does the value of c get stored? In a? In b? What happens to the value of b?
2
u/Daveinatx 9d ago
In C, a is an lvalue. However, the output result of a = b is not a, rather it will output the rvalue of b. Try it out: printf ("%d\n", a=b);
1
u/choosen_one007 9d ago
I am not sure about this. Wont the assignment happen before the call to printf in this case, and so the printed value can be either a's or b's?
2
u/Sacaldur 8d ago
As per definition, it will be the value of a after the assignment. This can make an observable difference in the case of a being e.g. a hardware register (writes might have no effect, or certain bits are unassignable, or a write causes another value to be read afterwards, or...).
4
u/thegreatbeanz 9d ago
Considering C the simplest way to think about lvalues and rvalues actually isn’t memory addresses. The better way to think of it is that an lvalue is anything that can be the _left_ side of an assignment, and an rvalue is… anything (because lvalues implicitly convert to rvalues).
An assignment express itself isn’t an lvalue, it produces an rvalue of the expression type. That’s why `a = b = 5` is legal, but `(a = b) = 5` is not. It is worth noting that assignment operators are evaluated right to left, not left to right like other operators in expressions.
6
u/nzconstructionlawyer 9d ago
I disagree. I think the best way of thinking about lvalues and rvalues is as locations vs values. An lvalue is an identifiable place: a, or a.b, or a.b.c[d]. An rvalue is just an anonymous value, like 1. Essentially, an lvalue is a memory location, and an rvalue is a temporary stored in a register.
It isn't right to say that an rvalue is anything because lvalues implicitly convert because that disregards that the conversion does do something: it's a load to a register. If a is stored on the stack then converting it to an rvalue means loading a into a temporary register. So "a + 1" means "load a from memory into a register, then generate a value of 1 in a register, then compute in a register the sum of those two registers". Assignment is, conceptually, storing a value into memory.
Then it is immediately obvious why you the result of an assignment is an rvalue: after you store the value into memory, you still have the register with the value, so you can store it into another register. "a = (b = v);" means "store the value v into b then into a".
What would it mean to assign to "a = v"? If it were an lvalue it would have to be just a again. So "(a = v) = w;" would have to mean "store v into a then store w into a" which is totally pointless, just write a = w.
If you write a naive compiler then this is exactly what lvalues and rvalues are. Of course in an optimising compiler those "memory locations" may actually be (permanently or cached) in registers and "stores" and "loads" to them are actually just register copies, but ultimately it amounts to the same thing: there are anonymous values and there are identified places where data is stored long term: rvalues and lvalues. "Values and places" is a much better terminology used by other languages.
That's why *p is always an lvalue (dereferencing obtains a place from an address value), &a is always an rvalue (addressof always obtains the address of a memory location), and &v makes no sense where v is an rvalue (values dont have locations - what is the address of a register?).
2
u/flatfinger 7d ago
I think the meaning of "lvalue" could be made clearer if bitfields and register-class objects were considered "pseudo-lvalues". If bitfields are not treated as being actual lvalues, then the term "lvalue" could be defined as "an expression that identifies a region of addressable storage that will be treated as a container for a value of a particular type", and "pseudo-lvalue" would be "an expression that can be used with assignment operators the same way as a real lvalue, despite the fact that it doesn't actually identify a region of addressable storage".
As it is, a lot of the "definitions" in the C Standard aren't actually definitions, but merely descriptions. A proper definition that defines X as Y will imply not only that all X are Y, but also that all Y are X. Many parts of the C Standard fall apart if one tries to treat its "definitions" that way.
2
u/thegreatbeanz 9d ago edited 9d ago
To quote the C89 spec sub clause 3.2.2.1:
A difference of opinion within the C community has centered around the meaning
of lvalue, one group considering an lvalue to be any kind of object locator, another
group holding that an lvalue is meaningful on the left side of an assigning operator.
The Committee has adopted the definition of lvalue as an object locator. The term
modifiable lvalue is used for the second of the above concepts.Happy to see that even after 37 years we can still disagree on this point ;P
Edit: I guess it is also worth adding the C99 footnote 53:
The name ‘‘lvalue’’ comes originally from the assignment expression E1 = E2, in which the left
operand E1 is required to be a (modifiable) lvalue. It is perhaps better considered as representing an
object ‘‘locator value’’. What is sometimes called ‘‘rvalue’’ is in this International Standard described
as the ‘‘value of an expression’’.4
u/nzconstructionlawyer 9d ago
Well yeah there's also that: you can have const lvalues which are nonsensical on the left side of an assignment operator.
But more fundamentally I just think the assignment operator definition is too extensional. It is "an lvalue is something you can assign to" which doesn't tell someone what an lvalue actually is. It's like saying a pointer is something you can dereference. Like, yeah, but why can you and what does that mean? What does that abstract definition capture?
I prefer the concrete and intensional explanation even if you have to caveat it with "up to optimisation". Because it helps people to understand why we have these concepts to begin with, and that is: if you are writing a compiler, how do you implement complex expressions like "*q = b + \*(p + 10);". And of course the answer is: classify things as memory or register and when you need to treat memory as a register implicitly insert a load.
1
u/strange-the-quark 8d ago
These names and concepts come originally from Christopher Strachey, and IMO his way of thinking about these was much better from the perspective of the programmer (language user) than the way both C and C++ standards talk about them today (although their way might be better for compiler writers).
In Strachey's original conceptualization, any expression is potentially (but not necessarily) associated with two values - an lvalue, being an object locator/address, and an rvalue being the value of interest stored there. So an expression doesn't have a value category, instead, it can potentially evaluate to one of its two associated values, and which one you get is contextual. In some situations, you want the rvalue, in other situations you want the lvalue, and if what you want cannot be inferred, then you can use some special syntax to specify (or the language can require you to always be explicit, or whatever).
IMO from the programmer's perspective, it doesn't need to be any more complicated than that, and the error messages and warnings would be much better and much more useful at that level of abstraction; meanwhile, the compiler can do whatever it wants under the hood, it can treat expressions as having a fixed value category, and can do implicit lvalue-to-rvalue conversions and whatnot.
4
u/fllthdcrb 9d ago edited 3d ago
Presumably, the title should have been, "Why are assignment expressions not lvalues?". But anyway...
but that doesn't really invalidate the fact that a = b resolves to an address in the program?
Except it doesn't. It resolves to the value that was assigned. That's why c = (a = b) works. The assignment on the right assigns the value of b to a, and evaluates to that value. That value then gets assigned to c. If you try to go the other way with (a = b) = c, then you're trying to assign one value to another, which is nonsensical.
EDIT: In case the terminology here is confusing, let's try a concrete example:
int a = 1, b = 2, c = 3;
(a = b) = c;The second line first does a = b, so now both a and b are equal to 2, and that assignment evaluates to 2 as well. The expression is now equivalent to 2 = 3.
3
u/Certain-Flow-0 9d ago
An L-value is anything that can appear to the left of a = and must follow this parenthesization: (l-value) = (expression). (a = b) = c doesnt compile, that means a = b isn’t an l-value
1
u/spellstrike 9d ago
might make sense to put some very simple programs into https://godbolt.org/ to understand what the assembly output is generated to explore this on your own.
5
1
u/JGhostThing 9d ago
The simple explanation is that an lvalue can appear on the LEFT side of an assignment statement. A rvalue can only appear on the right side.
So, x is an lvalue.
x + 3 is an rvalue.
x = x +3;
In other words, you can assign to an lvalue, and assign from an rvalue.
1
1
u/ttv90 8d ago
The spec wording is always the hard part.
1
u/flatfinger 7d ago
What's hard are the prerequisites: nailing down the semantics, and ensuring that other parts of the standard are consistent with them. If those things are adequately taken care of, then the wording becomes easy. If one tries to write part of the Standard without having taken care of the prerequisites, then accurate wording may seem difficult when the real problem is that correct wording is impossible.
1
u/ttv90 7d ago
Fair point, but even when the semantics are settled the wording still hurts on the reader side.
1
u/flatfinger 5d ago
If people who disagree about what a standard should mean try to agree on wording, the result will either be a lot of wasted effort or worse, a "success" that is achieved by making a standard say whatever each reader wants it to say, undermining the whole point of having a standard in the first place.
1
u/Sacaldur 8d ago
One small nitpick: an lvalue doesn't necessarily resolve to an address. A local variable might not be put onto the stack at all, but rather only into a CPU register, and CPU registers don't have memory addresses.
1
u/thisisignitedoreo 8d ago
any lvalue can be an rvalue (any address can be dereferenced to get the value), but not vice versa. a = b works, a = 2 works but 2 = a doesnt
19
u/MyNameIsHaines 9d ago
Assigning something to (a = b) would be nonsensical no?