Unexpected result after drop `&a`

Coming from R, Python, and some Swift, I'm fairly new to Rust. I'm reading The Rust Programming Language now and I thought I more or less understood memory allocation, references, and borrowing as explained in chapter 4.

Now, when I wrote the code, as below, I would have expected to get an error message when trying to print a, after dropping &a, which is not the case.

How can this be? Maybe it's a dull question, but being new to Rust, I don't understand this.

Thanks in advance for helping out.

let mut a = vec![1, 2, 3, 5, 8, 13];

    a.push(21);

    let b = &a;

    println!("{:?} and {:?}", a, b);

    drop(&a);

    println!("{:?}",a);
    println!("{:?}",b);

drop isn't particularly magic, it just takes whatever you give it by ownership and then doesn't give it back. Since you gave it a reference to a, it's exactly like calling any other function with &a: the function (drop) takes the reference for its duration, then you can keep using a after.

drop is defined simply as:

pub fn drop<T>(_x: T) {}

in the standard library.


In other terms, it did drop the &Vec<i32> you gave it. It's just that that doesn't do anything to your original Vec<i32>!

9 Likes

Thanks, @daboross this makes it clear to me.

1 Like

To clarify: what is the reason you expected this? Did you assume that a reference owns its referent? That is not the case — in Rust, references always point to/inside something that already exists/is already owned by someone else. You can't use a reference to extend the lifetime of a value (keep it alive longer than its scope would imply) nor to shrink it (you can't drop through a reference, as dropping implies ownership).

In Rust, it is not the case that every value is automatically heap-allocated/reference-counted/garbage-collected. References are simply pointers that point to wherever the object already is, without regard to how they were created or whether they are stack-allocated, heap-allocated, static, etc.

If you want a pointer type that owns its referent, use Box, or for reference counting, Rc. These types have runtime capabilities that references simply don't.

References are closer to "raw" pointers (in C and Rust alike) in that they don't actively do anything to the pointed value. The reason why Rust references are smarter than pointers in other systems languages is that they have metadata attached to them at compile time (and compile time only), such as lifetime and exclusive mutability requirements, which are tracked by the compiler. Once the compilation is over, however, none of this exists and it doesn't have any runtime consequences.

4 Likes

I, at least partly, mingled up heap en reference, at least that's what think now, after you elaborated on my question, for which I'm grateful. I probably assumed that dropping a reference equals dropping the part of the object that's allocated on the heap, or something like that.

As always you only become aware of your assumptions once things go wrong of in unexpected directions.

Thanks so much!

1 Like

This is precisely true if you substitute “Box” for “reference”. Box is the pointer type which deallocates from the heap exactly when it is dropped.

1 Like

Oke, I just saw that the concept of Box is dealt with in chap. 15, which is too far ahead. But does this mean that every variable that has some data allocated on the heap, also has this 'heaped' data 'boxed'? Because if I quickly scan chapter 15, boxing seems optional to me.

Thanks for the reply!

Box is how you use the heap — or the simplest way, more precisely. Other ways include Vec and Rc, and types that contain one of those inside themselves.

Sometimes heap allocation is necessary, not optional. But it's never implicit. There is always something done to use the heap, whether it's creating a Box, a Vec, or calling the allocator directly.

5 Likes

Rust has pointer types that don't look like pointers. Box<T> is a pointer. It has the exact same layout and number of bytes as &T. The difference is in their semantics: should the pointer be freed or not (is it owned, or borrowed). e.g. Box<str> is the owning version of &str. There's also Rc<str> and Arc<str> for shared ownership.

2 Likes

I don't understand what you are asking here. Putting things in a Box is "optional", insofar as calling it "optional" makes sense (it's a weird/not very useful way of putting it – after all, it is optional to use basically any language feature, but in certain situations, they are sometimes unavoidable).

It's is not the case that whenever a heap allocation "happens", then somehow by magic the contents of that heap block go into a Box. It is the converse: if you put something in a Box, then it gets heap-allocated. Box and other heap-allocating types are not magic1, there is no implicit type juggling going on. It's just that if you use a Box or a Vec or a String etc., they are going to make use of a heap allocation.


1: unfortunately, Box is magic w.r.t. how it's handled by the compiler. It's a so-called "language item", so the compiler knows about it, which is why you can e.g. move out of it even though it's a pointer-like type. This behavior isn't strictly necessary, though, and it doesn't have anything to do with whether it heap-allocates or not. I'm hoping one day Box will be un-special-cased from the language.

1 Like

This exactly answers my questions. Thanks a lot, @H2CO3 !