6 ms·
> how just adding `&` to appease the compiler is a common learner behavior. If learner is unfamiliar with the very idea of references, then his/her problem is
by ordu 4y ago
> how just adding `&` to appease the compiler is a common learner behavior.
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.
- Dylan16807 4y agoUnless something gets optimized away, I'm pretty sure "let a: i32" will allocate memory on the stack exactly like C. Do you have some sources?
- ordu 4y agoBy default new types in Rust are not Copy, so when you pass values of these types around they are moved, not copied. It leads for example to freedom to do something like: 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. [1] https://www.thecodedmessage.com/posts/cpp-move/ https://www.thecodedmessage.com/posts/cpp-move/
- ithkuil 4y agoIn rust moves are just copies that guarantee that no outstanding references to the old location can be held anywhere and thus the compiler is allowed to optimize away the copy. The same thing can be done by C/C++ compilers under some circumstances.
- Dylan16807 4y ago> 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. 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.
- ordu 4y agoIt seems to me that you believe, that there is some canonical representation of a source code by a machine code, and that optimizations are out of it. I do not think it this way. As I see it, compiler gets source code and generates machine code that have the same side-effects as I asked. Compiler is free to use any way to do it, while it doesn't change side-effects. 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.
- firstlink 4y ago> 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`. 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.