but there are operations in the language that expect a
place(T)
, and therefore do not perform the implicit conversion.one of these operations is assignment, which is like a function
place(T) -> T -> T
.it accepts a
place(T)
to write to, aT
to write to that place, and returns theT
written. note that in that case no read occurs, since the implicit conversion described before does not apply.void example(void) { int x = 0; x /*: place(int) */ = 1 /*: int */; /*-> int (discarded) */ }
another couple of operations that accept a
place(T)
is the.
and[]
operators, both of which can be used to refer to subplaces within the place.
now I have to confess, I lied to you. there are no places in C.
the C standard actually calls this concept “lvalues”, which comes from the fact that they are values which are valid left-hand sides of assignment.
however, I don’t like that name since it’s quite esoteric - if you tell a beginner “
x
is not an lvalue,” they will look at you confused. but if you tell a beginner “x
is not a place in memory,” then it’s immediately more clear!so I will keep using the Rust name despite the name “lvalues” technically being more “correct” and standards-compliant.
what’s interesting about
place(T)
is that it’s actually a real type in C++ - except under a different name:T&
.to begin with, in C we could assume that referencing any variable
T x
by its namex
would produce aplace(T)
. this is a simple and clear rule to understand.in C++, this is no longer the case - referencing a variable
T x
by its namex
produces aT&
, but referencing a variableT& x
by its namex
produces aT&
, not aT& &
!in layman’s terms, C++ makes it impossible to rebind references to something else. you can’t make this variable point to
y
:int x = 0; int y = 1; int& r = x; r = y; // nope, this is just the same as x = y
anyways, as a final
bossbonus of this blog post, I’d like to introduce you to thex->y
operator (the C one)if you’ve been programmming C or C++ for a while, you’ll know that it’s pretty dangerous to just go pointer-walkin’ with the
->
operatorint* third(struct list* first) { return &list->next->next->value; }
the conclusion here is that chaining
x->y
can be really dangerous if you don’t check for the validity of each reference. just doing one hop and a reference -&x->y
- is fine, because we never end up reading from the invalid pointer - it’s like doing&x[1]
. but two hops is where it gets hairy - inx->y->z
, the->z
has to read fromx->y
to know the pointer to read from.