Idea on how to create Unforgettable Types

Hi

So the other day while reading 1.63 release blog I noticed the inclusion of scoped types. By my understanding the reason we need to have scopes rather than a threadHandle, that waits until the appropriate thread is finished when dropped is because of the ability to forget in std::mem - Rust. As discussed in the nomicon Leaking - The Rustonomicon section we in general need to assume that a drop call may not happen. This as discussed in the nomicon leads to several complications when designing certain types. This lead to me wondering if there was a way to create types that gets around this issue.

So here's the idea:

We add a new auto trait

auto trait Forgettable {}

Which is implemented for all base types. I will call types which are T : !Forgettable a unforgettable type and make it undefined behavior to completely forget a unforgettable type (may still need it for unforgettable types in certain cases).

We also modify std's forget function into two functions (also need to alter some other std::mem functions in similar ways)

fn forget<T: Forgettable>(t:T) {
    // safe as T is forgettable
    unsafe { forget_unchecked(t) }
}

unsafe fn forget_unchecked<T>(t: T) {
    // same as current forget
    // ...
}

With the above implemented then I believe we have a Rust in which if a type T is unforgettable then there are four possible scenarios in regard to its destruction in safe code:

  1. Drop is called on T.
  2. T lasts forever e.g. memory leak.
  3. T is decomposed into its component parts.
  4. T is transformed with something like safe transmute.

Case 4 I assume if this was ever fully implemented will have a way to restrict such an ability for our type.

Case 3 can be managed with controlling access to our types and being careful with the methods we write for types.

Case 2 isn't avoidable I believe but if the type lasts forever it can be reflected by including a lifetime in the type. As this lifetime must be larger than the lifetime of T itself it should then become the 'static lifetime in the case (not 100% on this point, please correct me if incorrect) which can then be used in the bounds. (see scope type in std::thread for an example of this lifetime trick).

Case 1 is the case we want.

As such we can therefore write types which offer much greater guarantees and control in regards to how the are destroyed.

So is this actually possible or have I made a mistake somewhere. Any feedback or corrections to misunderstandings I've made would be appreciated.

Thanks

This concept is called linear types. Here's previous exploration of the problem:

8 Likes

This topic may be better suited for IRLO.

The idea comes up from time to time under the moniker "linear types", although what counts as a use varies.

I don't really understand what you mean here, but things with lifetimes can leak too. Example taken from this article.

5 Likes

Thanks for the reply and you're right on both points.

I actually didn't realize that there were two different forums.

For case 2 I was assuming we could do some lifetime tricks so that when a type with an lifetime (e.g. 'a in Foo<'a>) was leaked then that lifetime must become a 'static lifetime so we would therefore be dealing with Foo<'static> in the inputs.

But as you're safe_forget function in your example shows that is not the case. Hence the entire idea doesn't work.

Thanks again for pointing this out.

Your approach would work for the case of std::mem::forget() and std::mem::ManuallyDrop, but unfortunately they aren't the only ways to accidentally leak a destructor.

The canonical counter example is making a cycle of reference-counted pointers where two pointers keep each other alive even if all other references are dropped. There's no way to avoid such a situation without adding some sort of T: Unforgettable bound to all types with interior mutability (i.e. anything with a UnsafeCell), and that'd be a non-starter because it's not backwards compatible and would reject large swathes of valid programs.

2 Likes