If learner is unfamiliar with the very idea of references, then his/her problem is not an ampersand, but how to grasp the distinction between reference and value.
Rust makes it hard, because it takes its own view on how variable relates with its value: Rust variable holding a value behaves much more like a reference to the value than a variable in C does. It is hard to notice with Copy+Clone types, like integers, but it bites in the ass with others. Variable in C is associated with its memory. When you write `int a;` you create a variable which has `sizeof(int)` bytes of memory. It doesn't work with Rust, a place in memory is an attribute of value not of variable. `let a: i32` doesn't allocate memory, memory will come with a value, when you assign the value to `a`.
I mean it is hard to grasp at first. I came to Rust from a C world, and struggled to get this simple fact for a few months. It seems stupid in retrospect, it was written all over the place in Rust Book, but my mind took the relationship between a variable, memory and a value as a kind of an implicit dogma, and failed to notice how it breaks in Rust.
But I digress. So variable in Rust has some properties of a reference. And then there are "real" references like &, &mut, RefCell, Rc/Arc. These didn't made my life harder, they work like my C-mind expected them to work, but I assume that people who come from a higher level languages can struggle to grasp all the differences.
fn move_things(a: SomePrettyBigType) -> EvenBiggerType
When I was taught programming, I learnt that it is a bad thing, because passing big values to functions needs a lot of copying and is very slow. Not so with Rust. I personally did all the checks with emit=asm, and noticed how rustc passes an argument's address to a function (instead of copying an argument value) and one more address of a place, where function can build a value of EvenBiggerType.I never checked whether rustc preallocate memory for uninitialized variable or not. I bet it doesn't even in debug builds, because it knows in advance when variable is used. C compiler can work without this knowledge, but rustc cannot: it needs to check lifetimes and borrowing. Yes, it is normally seen as an optimization in C, but in rust many of C optimizations become a part of a language.
> Do you have some sources?
No, sorry, I have no source, my mental representation of rust code is a creation of my mind (being modest I'd add, that it is a second-hand creation probably, I'm not invented Rust, so someone thought of it before me, and I've read a lot what others think, probably I picked it up somewhere). But I can give you a link[1] to an article comparing C++ and Rust move semantics. There you can see the difference between "variable has no value" and "variable have a value that means 'moved out'". I see this difference as static vs dynamic tracking of "moved out" status (probably the article talks about it, I didn't reread it now and do not remember clearly). And I interpret it as a difference in a variable semantic: rust's variable has no memory, rust's values have. C++ variable on the contrary has a memory which is used to store value of the variable.
I see it as a semantic differences of variables in Rust, because this semantics works. I can predict machine code generated, I can predict when borrow checker will become unhappy, I can track lifetimes in my mind. I mean a scope of a variable and a lifetime of a value are different things. I cannot imagine how anyone can think of a lifetime of a value without believing that memory occupied by a value is an attribute of a value. And at my first months of dealing with Rust I really tried to find a way.
Local variables just get a slot on the stack. Allocation is done for several at once, and it would be a lot of work to handle it per-variable for negligible benefit.
It would be especially difficult to handle code that has multiple variables and moves one into a Box, chosen by a calculation, then keeps using the rest. I bet none of that memory is going to get reclaimed, it will just sit there marked "do not touch".
But that's all about a stack with optimizations and deliberately vague semantics. We can also learn something by looking at objects. Rust objects work like C++. To move a field out, you have to replace it with a placeholder version of that field.
I choose to think this way because real generated machine code are more like this. Maybe debug builds are close to an idea of a canonical machine code, but I do not know, because I see no reason to look at machine code of debug builds. Moreover code without optimizations applied is harder to read with all that noise of unnecessary actions.
> Local variables just get a slot on the stack. Allocation is done for several at once, and it would be a lot of work to handle it per-variable for negligible benefit.
Local variables often can be not allocated at all. They can be eliminated by a compiler. I hold a view that it is incorrect to talk about variables at all if we talk about generated machine code. It is just like with a handcrafted assembler code: a counter can be pushed on the stack, then popped, increased, used as an index into an array, then pushed back to be popped once more to check should we do one more iteration of this. It is incorrect to say that counter is on the stack or it is in a register because sometimes it is here and sometimes it is there. The counter exists only in my mind, not in the code. When another person can deduce from my code the existence of the counter it is not because the counter exists in the code, it is because another person can deduce what I was trying to do.
Look at this:
let s = SomeBigStruct::new();
return Box::new(s);
With any luck s can be eliminated, value returned by SomeBigStruct::new() will not be stored on the stack at any point. And it is not the thing we can ignore, because SomeBigStruct can be very large and it may be not a good idea to place it on the stack or to spend time copying it from the stack into a Box. Rust allow us to eliminate variably ourselves: return Box::new(SomeBigStruct::new());
but it is the same basically: SomeBigStruct::new returns a big value which is then moved into a Box. If rustc was unable to move this value from SomeBigStruct::new into a Box without a call to memcpy, then Rust would be an academical curiosity, not a practical language at all. But sometimes I need to store SomeBigStruct in an intermediate variable for some reason, and Rust must be able to avoid memcpying nevertheless.> It would be especially difficult to handle code that has multiple variables and moves one into a Box, chosen by a calculation, then keeps using the rest.
Yeah, there are different situations and different situations need different methods.
> But that's all about a stack with optimizations and deliberately vague semantics. We can also learn something by looking at objects.
Objects are objects. They are not variables, they are values. Structure field name is not a variable, it is a way to deal with a value of a variable. When I create a struct or an array, I allocate memory for that value, not for a variable.
Well that's literally how C works. There's an "abstract machine" that runs your code, and that's how you know what side effects it's obligated to produce. Though the C abstract machine isn't very concerned with memory locations, so it's not particularly relevant to this discussion.
> Local variables often can be not allocated at all. They can be eliminated by a compiler. I hold a view that it is incorrect to talk about variables at all if we talk about generated machine code.
I'm aware that variables can be optimized out. But so can values! You can have a value in your program that takes up 0 memory even though it gets used to do something.
If we want to talk about variables or values correlating with memory, I think we should ignore optimizations like this. And I really don't think they provide evidence toward a claim that values have memory but variables don't.
> Structure field name is not a variable, it is a way to deal with a value of a variable.
You have a much more narrow definition of "variable" than I do. It has a name, it can hold a value... But fine, let's focus on the kind you consider variables.
> With any luck s can be eliminated, value returned by SomeBigStruct::new() will not be stored on the stack at any point.
> Yeah, there are different situations and different situations need different methods.
Let me try phrasing my argument a different way.
It's often easy for the compiler to avoid allocating memory on the stack at all. But your claim was that you could have uninitialized variables with no memory allocated, then initialize them, and they would now have memory allocated.
What's some code that does that?
Putting something in a Box isn't that. A Box has two parts, the small Box struct itself, and the memory it allocates for the contents. If you have a Box variable, it will be given space for the struct even before you run Box::new. (With the caveat that the compiler might try to shrink the scope where the variable exists, so you might need to do a little bit to prevent that.)
The worst part of C abstract machine in Rust is lifetimes. Lifetimes becomes hard to think of, borrow checker becomes impossible to satisfy. You move value from x to y, therefore you move memory, now you do not need to track a lifetime or a scope of x, it is irrelevant, it have no memory, it doesn't exist anymore. But you need to track y, because it owns some value, which is the memory. Lifetimes of values are different from scopes of variables, but it is lifetimes that matter to a borrow checker.
And now a lot of words about how I manage to think this way. I'm not sure it is relevant to the discussion, but because I already had typed it and if you might be interested...
> Well that's literally how C works. There's an "abstract machine" that runs your code, and that's how you know what side effects it's obligated to produce. Though the C abstract machine isn't very concerned with memory locations, so it's not particularly relevant to this discussion.
Yes, thinking about it, I agree. With C it is natural to believe in a canonical representation. I thought exactly this way before I moved to rust. Possibly I have in my mind some representation of this even for rust, but I'm not sure. Oftentimes I do not think of functions as of sub-programs that must be called, because mostly they are inlined. Functions are chunks of code that are combined into one to compose a working code. I do not think of variables as of something existing in a machine code, they are compile-time abstractions allowing me to relate to ideas of how to compute their values or to the values itself (it feels like lazy-evaluation mode of thinking, and it is, but evaluations in Rust are not always lazy).
> It's often easy for the compiler to avoid allocating memory on the stack at all. But your claim was that you could have uninitialized variables with no memory allocated, then initialize them, and they would now have memory allocated.
Not necessarily. A value can live in the memory of compiler. It can be computed statically and then used without really forming a structure or something. When you move a value of variable into a Box, it can easily happen. Value was calculated, but have no representation in the machine code, but then when Box is initialized, machine code does a few "mov"s of immediate values into Box.
> Putting something in a Box isn't that. A Box has two parts, the small Box struct itself, and the memory it allocates for the contents. If you have a Box variable, it will be given space for the struct even before you run Box::new.
No, it will not get space before Box::new
struct Point {
x: i64,
y: i64,
}
impl Point {
fn new(x: i64, y: i64) -> Self {
Self {
x: x,
y: y,
}
}
}
#[inline(never)]
fn make_a_point() -> Box<Point> {
let p = Point::new(2, 3);
Box::new(p)
}
How does it translates into a machine code? _ZN3tmp12make_a_point17h643905a77db82570E:
.cfi_startproc
pushq %rax
.cfi_def_cfa_offset 16
movl $16, %edi
movl $8, %esi
callq *__rust_alloc@GOTPCREL(%rip) ; <-- allocation, now %rax is a pointer to uninitialized Box<Point>
testq %rax, %rax
je .LBB10_1
movq $2, (%rax) ; <--- (%rax).x = 2;
movq $3, 8(%rax) ; <--- (%rax).y = 3;
popq %rcx
.cfi_def_cfa_offset 8
retq
See? There were not place to store Point { x: 2, y: 3 } before Box was created. And the only place in a program it exists as a Point (which can be addressed for example) it is a memory owned by a Box. So Point::new is evaluated after Box::new, and then inlined Point::new initializes not the memory on the stack but memory of the Box directly. In other case compiler might pass %rax into a constructor as a kind of `this` pointer in C++, so it could initialize Point directly in the memory owned by the Box.%rax holds "second part" of the Box, "the small Box struct itself", which is just the address of the memory owned by the Box. We can think of %rax as of variable holding the Box, though there is no corresponding variable in the source code.
> With the caveat that the compiler might try to shrink the scope where the variable exists, so you might need to do a little bit to prevent that.
Why may I want to do this? I mean, it is an important trait of Rust to avoid copying while moving. It wouldn't work if it was necessary to allocate structure on the stack before moving it into the box. Theoretically it is ok, but what about performance of the code? C++ deals with it using `this` pointer, and a special syntax for calling a constructor. Rust doesn't do it and constructor returns object "by value" and to get performance on par with C++ it is necessary for rustc to optimize it, so the machine code of the constructor was able to initialize the target memory directly.
Yeah, it is the optimization, we can probably ignore it. But... but I cannot ignore it, because it is important. I need to know that my code doesn't do easily avoidable copying. So I think of it as of part of the language itself.
> I'm aware that variables can be optimized out. But so can values! You can have a value in your program that takes up 0 memory even though it gets used to do something.
Above there is an example for this. The Point comes to an existence at some point, while variable p doesn't. So variables do not exist at run time, while values do.
> I think we should ignore optimizations like this.
Why? Just to protect a representation of an abstract machine? It mostly works with C, and so it makes sense with C. But it mostly do not work with Rust, it doesn't predict a generated machine code well. For example, in C you can rely that function will be called, one needs to explicitly inline it to avoid the call. In Rust you need to explicitly forbid inlining to get a call instruction reliably (I did it in the example above to separate the code of make_the_point from main function). In C you can rely on Point being created before a memory malloc'ed, and you can infer from that that Point must be stored somewhere, probably on the stack. In Rust as you can see it is not the case.
> It's often easy for the compiler to avoid allocating memory on the stack at all. But your claim was that you could have uninitialized variables with no memory allocated, then initialize them, and they would now have memory allocated.
It is not just that. If you "deinitialize" variable by moving value out of it, it loses its memory.
let p1 = Point::new(1, 2);
let p2 = p1;
There is just one Point allocated. Not two. In C we would think of it as of two Point-sized slots on the stack, and about value being copied from one to another. But here you should think of it as of one Point-sized slot which was named p1 before we moved it to p2, after which p1 is not associated (lexically bound) with the memory slot of the Point, p2 does.> You have a much more narrow definition of "variable" than I do. It has a name, it can hold a value...
But it works differently. It pins field memory to the containing struct. One cannot just change association by moving:
let p = struc.point;
Rust will forbid it, we will need to use Option<Point> instead of Point as a type for the field, or to find some other way around it. Though I suspect it is harder now to learn because borrow checker becomes smarter and it allows to move out of a struct when it sees that lifetime of the struct is bound and it will not be used anymore.But the code you wrote there isn't the kind of thing I was talking about. Let me try to do a better job explaining. After a bunch of trouble with the borrow checker, here is some code:
#[derive(Debug)]
struct Point {
x: i64,
y: i64,
}
impl Point {
fn new(x: i64, y: i64) -> Self {
Self {
x: x,
y: y,
}
}
}
use rand::Rng;
extern crate alloc;
fn rand() -> u8 {
let mut rng = rand::thread_rng();
rng.gen()
}
fn test() {
let v1;
let v2;
let v3;
let v4;
let n: u8 = rand();
let address;
if n<30 {
v1 = vec![Point::new(2, 3)];
address = &v1 as *const Vec<Point> as usize;
} else if n<60 {
v2 = vec![Point::new(2, 3)];
address = &v2 as *const Vec<Point> as usize;
} else if n<90 {
v3 = vec![Point::new(2, 3)];
address = &v3 as *const Vec<Point> as usize;
} else {
v4 = vec![Point::new(2, 3)];
address = &v4 as *const Vec<Point> as usize;
}
println!("{:#x}", address);
println!("{:p}", &address);
println!("distance: {:#}", (&n as *const u8 as usize) - address);
}
fn main() {
for _ in 0..30 {
test();
}
}
The assembly was annoying to go through so I printed the stack addresses instead. And I used Vec because it's bigger.This code creates 4 vec variables. It randomly picks one to put a Vec with a Point into, and leaves the other 3 uninitialized.
Then whichever one got initialized has its address stored into "address".
Then it prints out that address, and the address of "n", the next thing on the stack. And it prints out the difference to be extra helpful.
I ran this on the rust playground.
2/3 of the time it uses v4. And 2/3 of the time it prints 31 as the distance.
The rest of the time is evenly split between v1, v2, and v3. And the rest of the outputs are randomly split between 55, 79, and 103.
Therefore I conclude that it allocated space for all four variables at the start of the function, even though there was only one value.
A variable doesn't take up space when the compiler can be clever enough to make it not exist at that moment in time. But that only happens for some code. It's easy to find cases where variables exist as real tangible chunks of memory, even though there aren't values. If you try to touch them, the borrow checker will complain extremely hard.
So here is how I would summarize things: Sometimes the compiler can avoid having variables exist at runtime. Sometimes the compiler can avoid having values exist at runtime. Very often it will reduce the amount of time a variable exists to avoid memory use and to avoid moves and copies. And very often a variable will actually be the final resting place of some value, rather than a default location on the stack. But variables are not just a compile-time concept. They can manifest as chunks of possibly-empty memory when the compiler isn't clever enough or when there's not enough benefit to being clever.
> Above there is an example for this. The Point comes to an existence at some point, while variable p doesn't. So variables do not exist at run time, while values do.
That's not an example of a value not existing at run time. I mean something like setting n=pi*2, then printing n with 2 digits of precision. If the compiler is having a good day, you'll see code that directly prints "6.28" and the value of pi*2 will never exist at run time.
In the most literal sense, neither values nor variables have to exist. Only side-effects exist, and the program could have a completely different implementation from what the source code suggests.
This just isn't accurate at all. In the corresponding abstract machines, `int a = 0;` and `let a = 0u32;` mean the exact same thing. The only thing that move semantics change is that variables become unassigned when moved from (unless Copy-typed), and the rust frontend tracks unassigned variables and doesn't permit their use. This has nothing to do with memory allocation.
It turns out that where the AM allocates and where optimized code allocates are often wildly different, but do not mistake this for a difference in the languages. If I had to guess, (optimized) C was behaving differently than you believed, but you only noticed this when it came up in rust.
> Take the first two sections as a perfect example. You decide you want to introduce a single-character control symbol `&` (which is questionable by itself), how do you apply it? In one case you decide that this control symbol indicates that a value is immutable and in another it's part of the declaration of a value as explicitly mutable. You've now introduced a semantic contradiction that a learner has to overcome.
Here you write:
> The author then explains `&var` and `&mut var` not in terms of their nature as references but in their aspects of mutability.
- The angry, tired dog was running
- The angry dog was running
Does the “angry” cause confusion when trying to understand what “tired” means?
`&` never has anything to do with mutability. Only `mut`.
There is no “semantic” contradiction. “Weltschmerz” is a word, and “Welt” and “smerz” are two other words; the fact that they can occur in a compound noun doesn’t mean that “Welt” and “schmerz” by themselves get contaminated by each other.
The fact that they chose to use `&mut` is… syntactic. The semantics would be the same if they used `& mut` or `reference mutable` or `reference which is mutable`.
I’ve sprinkled some ampersands around Rust code in my little time but never because I was confused about how it interacted with mutability.
That said, I think you downplay the ways that references and mutability do interact in Rust. If there is a single outstanding reference to a value, Rust won't let anyone mutate the underlying value. I think this is quite an important part of Rust to understand that's not obvious to someone who's new and especially coming from a language that enforces no such guarantees about mutability.