Why do I need to use pointers for non-primitives (BEGINNER)

Hello,

I am a n00b so pardon my ignorance. I am wondering why specifically non-primitive variables require me to use a reference (pointer - &) when assigning them as the value of another non-primitive variable. On the other hand, primitives can be assigned to, using just a simple '=' sign. Is this a particular design choice or is there something I am missing?

Thanks

//Primitive array
let arr1 = [1,2,3]
let arr2 = arr1;

vs.

//Vector
let vec1 = vec![1,2,3];
let vec2 = &vec1;

You can use = for both cases. = is a "move", while & is a "borrow" (or reference), and both work for both the primitive array case and vector case.

I think chapter 4 of the book would offer a good explanation (if I'm to explain here it'd just be a duplicate of what's already in the book).

As a side remark, we call & and &mut as references rather than pointers to avoid confusion, as they carry vastly different semantics than C/C++ pointers, and we do have raw pointers in unsafe Rust which would be closer to C/C++ pointers.

3 Likes

The confusion may also arise from the following difference (mem::drop is just an example "usage" of the value). This compiles fine:

let arr1 = [1,2,3];
let arr2 = arr1;
mem::drop(arr1);

This compiles fine too (but would be an error in old versions, without NLL):

let vec1 = vec![1,2,3];
let vec2 = &vec1;
mem::drop(vec1);

But this will generate a compilation error:

let vec1 = vec![1,2,3];
let vec2 = vec1;
mem::drop(vec1); // error: use of moved value

The difference is that primitives are Copy and so can be used after being assigned to other binding (i.e. variable), but heap-allocated types, such as Vec, are not. If you really need two values containing the same vector, you must explicitly Clone it:

let vec1 = vec![1,2,3];
let vec2 = vec1.clone();
mem::drop(vec1);

Playground with all four cases

1 Like

Is this due to their heap-allocated nature? Why do non-primitives require you to clone?

This is because a Copy will memcpy the data bit by bit, which is okay for primitives, given that they hold data which upon Dropping would not allow for a read-after-free:

  • [[u | i][size | 8 | 16 | 32 | 64 | 128] | f[32 | 64] | char | bool] can all be literally represented by a number. Copying that number is okay, as though it won't do very much when it is dropped (It will deallocate the memory belonging to just that number and that's it)

On the other hand, if we look at the implementation for Drop for Vec for example, (Which is what String internally uses), we'll see that it de-allocates the data it holds no matter what, meaning that the data under the Vec is now freed. Therefore, if we copy the Vec bit by bit without regard for its contents, we'll see that we could free its data and we end up with an invalid pointer:

//Imagine we could:
impl<T> Copy for Vec<T> {}

//Therefore
let a = vec![1, 2, 3];
let b = a; //Copying it here

assert!(ptr::eq(a.as_ptr(), b.as_ptr())); //They both point to the same data

drop(a); //Now the data is deallocated

b[0]; //This is undefined behavior!

Therefore we can't impl Copy any of the following:

  • Vec/HashMap/HashSet
  • String
  • Box
  • Rc/Arc - We can't attach a hook onto Copy
  • etc... etc.. etc..
1 Like

Yes, it's essentially about whether they're on the heap or not in this case, although that's not necessarily the full picture. It's not really about if it's primitive, because you can implement very large structs which can be Copy.

Everything from this section of the docs and below is good read: https://doc.rust-lang.org/std/marker/trait.Copy.html#whats-the-difference-between-copy-and-clone

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.