General questions about the Rust object model and how that affects lifetimes

I was reading the implementation of std::vec::Vec's truncate method and I noted that it destructs the tail portion of the array by simply interpreting that region of storage as a slice and then calling drop_in_place on it.

I'm an experienced C++ programmer and this fascinated me because this would be 112% UB in C++.

So it leads me to wonder, what's the specification of Rust's lifetimes and object model that allows this?

It seems to me that Rust is permissive enough such that one can "bless" an array object into existence over a region of storage and then drop it.

C++'s equivalent of drop_in_place(foo) is foo->~T(). foo is now reachable but interacting with it is UB. Importantly, this does not call the deleting dtor, and delete foo is now UB. Since C++ does not have a meaningful version of forget, such an operation is in general UB, but not for the reason you think:

{
  A a;
  A* p = &a;
  p->~A();  // First call to ~A().
}  // Second, implicit call to ~A(). This UB.

Equivalent Rust, which is also UB, is as follows:

unsafe {
    let mut a: A = ...;
    ptr::drop_in_place(&mut a);  // First call to A::drop().
}  // Second, implicit call to A::drop(). This is UB.

This Rust code can be made correct by adding mem::forget(a); at the end.

There is also the fact that C++ does not have an equivalent of [T] that a destructor can be called on. The closest analogue, std::span, is trivially destructible. What Rust is doing is not equivalent to operator delete[].

ptr::drop_in_place::<[T]> can be thought of as just iterating over the elements and calling drop_in_place on each; a raw [T] owns its values but not its allocated space (just like any other non-reference type). Since Vec never actually holds references to the underlying memory, and only creates them dynamically after checking that the pointed-to memory is valid, there is no UB. This is the exact same process by which std::vector<T> is implemented in all major STLs, except C++ can take advantage of operator new[] to make the code a bit more straightforward.

6 Likes

Ah, I think I'm starting to understand a bit better now.

Because internally, Vec stores a pointer to raw storage, there's no objects or formals types to track.

Rust permits the programmer to simply "bless" objects into existence by binding a reference to a region of storage.

Very fascinating!

Out of curiosity, how does Rust internally handle dynamically sized types like [T] in these cases?

1 Like

Two things to note:

  1. This discussion is better suited for https://users.rust-lang.org/. This forum is dedicated to the development of Rust itself.
  2. If you've implemented a custom container in C++ that uses placement new (probably with std::aligned_storage) and had to manually invoke the desctructor (like @mcy discusses), then that is sort of how Rust's std::vec::Vec works (some of the specifics are a bit different since Rust doesn't have placement new, but some of the general ideas are similar).
2 Likes

With a big asterisk, that eeeeeh using a value after dropping it, even if it's just to move it into mem::forget, isn't necessarily safe.

Instead, you should put it in a ManuallyDrop first, and then use that to handle the manual dropping.

As an example, consider Box<T>. When passed around, Rust adds the LLVM @dereferencable attribute to it. If it's been dropped (in place) already, this is UB, because it isn't dereferencable anymore, it's dangling.

I have no idea what you're allowed to do with a value after you've dropped it in Rust, formally. That's a question for the unsafe code WG to figure out (if they haven't already made a proposal). At best, it's similar to in C++ when you've std::moved from a value: you have to know the specific type and its own guarantees to do anything more than (in C++: destruct or move (into); in Rust:) deallocate it without touching.

5 Likes

Ah, my bad. I came here because I have a lot of questions about how Rust itself works and I wasn't sure where to go. I figured the internals forum would be a good place to ask.

Let's talk about how to make this happen too :stuck_out_tongue:

You and your fancy modern Rust types. =P

Yes, that is technically correct. I wrote that post with my C++ language lawyer hat on, so I wasn't going for precision on the Rust side. As @mjbshaw correctly points out, the "real" way to do something like this these days in C++ is to build an analogue of ManuallyDrop using std::aligned_storage and then do placement new hilarity.

Without how many people keep linking that RFC, I'm starting to think I should run a petition :stuck_out_tongue:

Oh, you're far from alone there :slight_smile:

These days maybe half the feature requests I see people make are for things already in the pipeline, and their google fu simply didn't find it for whatever reason.