Understanding of ownership for pointers

I have recently read the article by Arthur Carcano about transient droppers and something is bugging me about the code sample he provides :

let mut ptr_i = uninit_array.as_mut_ptr() as *mut T; // Look here...

let mut transient_dropper = TransientDropper {
    base_ptr: ptr_i, // Here...
    initialized_count: 0,
};
let array : Option<[T;5]> = unsafe {
    for i in 0..5 {
        let value_i = f(i).unwrap();
        ptr_i.write(value_i); // here
        ptr_i = ptr_i.add(1); // and here
        transient_dropper.initialized_count += 1;
    }
    mem::forget(transient_dropper);
    Some(uninit_array.assume_init())
};

The code sample compiles fines so this is a post to align my view of ownership not to criticize his work.

If you look at my comments from top to bottom, here is my issue :
"How is it possible that he defines a value (even if it is a pointer), assign it to a field of a structure then proceeds to modify it twice outside of the said structure (he is not accessing the value from the "struct namespace") ?"

Should not the value have moved after having assigned it to the structure ?

I have read the write function's documentation but it says "Overwrites a memory location with the given value without reading or dropping the old value" so it does tell me that it does not affect the pointer (and by then not consuming it ?) but is not the ptr_i = ptr_i.add(1); doing exactly that (as its declaration consumes self : fn add(self, count: usize) ...)?

Once again I am trying to piece out the details, not making any claims. I do not know what rules apply in unsafe blocks or to pointers so I am trying to acquire knowledge.

Generally, yes. In the case of *mut T pointers, they implement the Copy trait, just like e. g. integers, and thus only a copy of ptr_i is moved into transient_dropperʼs base_ptr field, while the original ptr_i remains usable. The incrementing only happens to prt_i, while the copy inside of the transient dropper remains unchanged, still pointing to the first element in the array. If you read the Drop implementation of TransientDropper, you will find that it assumes exactly that: that base_ptr still points to the first array entry.

1 Like

It makes total sense now. Sorry I didn't see the Copy trait in the docs.

@TheSirC know that your concern is legitimate, since if these had been Rust mut / exclusive references we would have had a violation of the Stacked Borrows model:

example
use ::core::mem::MaybeUninit as MU;

let mut slice_mut: &mut [MU<T>] = …;
let mut transient_dropper = {
    struct TransientDropper<'__> {
        slice_mut: &'__ mut [MU<T>],
        initialized_count: usize,
    }
    impl Drop for TransientDropper<'_> {
        fn drop (self: &'_ mut Self)
        {
            unsafe {
                ::core::ptr::drop_in_place(
                    self.slice_mut[.. self.initialized_count]
                        .assume_init_mut()
                )
            }
        }
    }
    TransientDropper { slice_mut, initialized_count: 0 }
};

let array: Option<[T; 5]> = unsafe {
    for i in 0 .. 5 {
        let value_i = f(i).unwrap();
        slice_mut[i] = MU::new(value_i); // here
        transient_dropper.initialized_count += 1;
    }
    mem::forget(transient_dropper);
    Some(uninit_array.assume_init())
};

we get:

error[E0503]: cannot use `*slice_mut` because it was mutably borrowed
  --> src/lib.rs:36:13
   |
30 |         TransientDropper { slice_mut, initialized_count: 0 }
   |                            --------- borrow of `*slice_mut` occurs here
...
36 |             slice_mut[i] = MU::new(value_i); // here
   |             ^^^^^^^^^^^^ use of borrowed `*slice_mut`
37 |             transient_dropper.initialized_count += 1;
   |             ---------------------------------------- borrow later used here

error[E0506]: cannot assign to `slice_mut[_]` because it is borrowed
  --> src/lib.rs:36:13
   |
30 |         TransientDropper { slice_mut, initialized_count: 0 }
   |                            --------- borrow of `slice_mut[_]` occurs here
...
36 |             slice_mut[i] = MU::new(value_i); // here
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assignment to borrowed `slice_mut[_]` occurs here
37 |             transient_dropper.initialized_count += 1;
   |             ---------------------------------------- borrow later used here

In this case, however, they were not using &mut references, but *mut pointers. These carry less invariant requirements that &mut references do. For instance, they only need to be unaliased / exclusive when write-dereferenced, contrary to &mut references, which need to be unaliased / exclusive while alive (while usable, to be precise).

That being said, that code could be made compatible with Stacked Borrows and exclusive references with a simple solution: to derive / reborrow the ptr used in the loop from the transient_dropper, which gets to be the intermediary:

  let mut transient_dropper = {
      // ...
      TransientDropper { slice_mut, initialized_count: 0 }
  };
+ let slice_mut = &mut *transient_dropper.slice_mut; // reborrow
  let array: Option<[T; 5]> = unsafe {
      // ...

Finally, the article mentions ::scopeguard, and I find it to be a very handy tool to do this with far less unsafe. Indeed, they offer an API whereby the drop-guard takes ownership of some value (here, the slice_mut), but still DerefMuts to it while alive, so that the nested reborrow does not contradict SB rules:

  • let mut transient_dropper = ::scopeguard::guard(
        // state
        (slice_mut, 0),
        // drop
        |(slice_mut, initialized_count)| {
            ::core::ptr::drop_in_place(
                MU::slice_assume_init_mut(
                    &mut slice_mut[.. initialized_count]
                )
            )
        }
    );
    let (ref mut slice_mut, ref mut initialized_count) =
        *transient_dropper // DerefMut
    ;
    

Aside

Nice article @krtab!

3 Likes