-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RFC: Add MaybeDropped<T>
#3918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ionicmc-rs
wants to merge
4
commits into
rust-lang:master
Choose a base branch
from
ionicmc-rs:maybe-dropped
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
RFC: Add MaybeDropped<T>
#3918
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,335 @@ | ||
| - Feature Name: `maybe_dropped` | ||
| - Start Date: 2026-10-2 | ||
| - RFC PR: [rust-lang/rfcs#3918](https://github.com/rust-lang/rfcs/pull/3918) | ||
| - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
|
||
| ## Summary | ||
| [summary]: #summary | ||
|
|
||
| A wrapper type for values that may have already been dropped. | ||
|
|
||
| This is similar to `mem::MaybeUninit` in that it can represent invalid data when the inner value is not needed, but implies a different lifecycle for the contained value. | ||
|
|
||
| Note this type does not provide safe access to `T` and does not run destructors when dropped | ||
|
|
||
| ## Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| Some types/systems do not always require the value be active for their entire duration. In these cases, an `Option<T>` is often used. | ||
|
|
||
| However, an `Option<T>` here is correct, but the following issues arise: | ||
|
|
||
| 1. It is misleading here. | ||
|
|
||
| While an `Option<T>` works, its not truly an `Option`. For example, the type should not be instantiated as `None`, and the value should only be | ||
| `None` after it's usage is completed. | ||
|
|
||
| Take this example. | ||
|
|
||
| ```rust | ||
| pub struct Fuse<I: Iterator> { | ||
| done: bool, | ||
| iter: I | ||
| } | ||
| ``` | ||
|
|
||
| The actual iterator is not needed after it yields `None` once. So in this case, We can replace the `I` with a `MaybeDropped`, which can optimize code by dropping the value early, since we track if it is done elsewhere. | ||
| (eg. by freeing allocator space, releasing a lock, etc.) | ||
|
|
||
| 2. it has no additional storage. | ||
|
|
||
| `MaybeDropped<T>` has the same memory layout as `ManuallyDrop<T>`, which has the same memory layout as `T`. This can allow optimizations if | ||
| there is no need to store the drop state of `T`. | ||
|
|
||
| Additionally, types/systems which would need `MaybeDropped` likely already have a means of tracking whether `T` is still needed or not | ||
| elsewhere. Storing it again (for example, using `Option`) is wasteful of memory. | ||
|
|
||
| ### Why this should be seperate from `MaybeUninit` | ||
|
|
||
| `MaybeUninit` implies a value is initialized once and never goes back, having the following lifecycle. | ||
|
|
||
| - MaybeUninit created (possibly uninit) | ||
| - MaybeUninit is initialized/confirmed to be already initialized. | ||
| - it stays initialized. | ||
|
|
||
| `MaybeDropped` on the other hand implies the following lifecycle, where data is created in initialized state and later dropped. | ||
|
|
||
| - MaybeDropped created initialized or in a `already dropped` state | ||
| - The MaybeDropped is used. (if not dropped) | ||
| - it is dropped. (if not dropped) | ||
| - it stays dropped (not written to) | ||
|
|
||
| ### Advantages over `T` | ||
|
|
||
| For some cases, early drop is *required*; for example: | ||
|
|
||
| - Locks | ||
| - Allocations (in performance critical code) | ||
| - Mechanisms requiring a value is dropped. | ||
|
|
||
| `T` does not permit a dropped state, for this reason `MaybeDropped` is used. | ||
|
|
||
| ### Usage in FFI | ||
|
|
||
| `MaybeDropped<T>` is also FFI safe if `T` is FFI safe, this allows for an FFI safe transfer of data that has possibly already been dropped. | ||
| This can also allow for an FFI safe `Option`, with additional argmuents | ||
|
|
||
| ## Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| ### Usage | ||
| Users would use `MaybeDropped` just like a `MaybeUninit`, in reverse. | ||
|
|
||
| Take this example: | ||
|
|
||
| ```rust | ||
| struct OnceImage { | ||
| loader: MaybeDropped<ImageLoader>, | ||
| stored: Option<Image> // drop state of `loader` is stored here. | ||
| } | ||
| ``` | ||
|
|
||
| suppose `ImageLoader` has alot of open resources: | ||
|
|
||
| - locks | ||
| - OS requests | ||
| - File Descriptors | ||
| - Services | ||
|
|
||
| the `loader` is only used once, while `OnceImage` can be long-lived. It makes no sense here to keep the loader for the lifetime of `OnceImage`. | ||
| And `stored` tracks the drop-state of `loader`, so it would be redundant to store it twice. | ||
|
|
||
| #### Where to use `MaybeDropped` rather than `MaybeUninit` | ||
|
|
||
| The seperation is clear. | ||
|
|
||
| `MaybeDropped`: Values which are initialized, and the dropped later. | ||
|
|
||
| `MaybeUninit`: Values which may not be initialized yet. | ||
|
|
||
| #### Usage in FFI | ||
|
|
||
| `MaybeDropped` can be used with `bool` for FFI safe `Option`s | ||
|
|
||
| ```c | ||
| // c code | ||
|
|
||
| // extern definition... | ||
|
|
||
| void do_some_work() { | ||
| // ... | ||
| rust_work(possibly_dropped, is_dropped); | ||
| } | ||
| ``` | ||
| And then, in rust | ||
| ```rust | ||
| // imports... | ||
|
|
||
| #[unsafe(no_mangle)] | ||
| extern "C" fn rust_work(dropped: MaybeDropped<SomeStruct>, is_dropped: bool) { | ||
| let as_option = if is_dropped { | ||
| None | ||
| } else { | ||
| Some(dropped.assume_alive()) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Undefined Behaviour in `MaybeDropped` | ||
|
|
||
| The following is *__not__* undefined behaviour: | ||
|
|
||
| - Creating a `MaybeDropped` that is already dropped. (`MaybeDropped::dropped`) | ||
| - Dropping a `MaybeDropped` (so long it is the first time) | ||
| - Obtaining raw pointers to the inner memory. | ||
|
|
||
| the following *__is__* undefined behaviour: | ||
|
|
||
| - Dropping a `MaybeDropped` twice. | ||
| - Creating an uninitialized `MaybeDropped` (where `T` cannot be unintialized) | ||
| - any access to `T` after drop. | ||
| - Obtaining references to `T` after drop. | ||
| - Writing a value to the `MaybeDropped` after it has already been dropped. | ||
|
|
||
| #### Examples | ||
| ```rust | ||
| let dropped = MaybeDropped::<i32>::dropped(10); // creating a logically dropped `MaybeDrop` without a value to drop would be the same as making it uninitialized, which isn't always valid. | ||
|
|
||
| // this is not ok | ||
| // let uninit = MaybeUninit::<MaybeDropped<i32>>::uninit().assume_init(); | ||
| // this is ok (ZST) | ||
| let uninit_unit = MaybeUninit::<MaybeDropped<()>>::unint().assume_init(); | ||
|
|
||
| // this is not ok | ||
| // let reference = dropped.assume_alive(); | ||
|
|
||
| // this is ok | ||
| let ptr = dropped.as_ptr(); | ||
|
|
||
| // this fails to compile (not mutable), but is otherwise ok UB-wise | ||
| // let mut_ptr = dropped.as_mut_ptr(); | ||
|
|
||
| let to_be_dropped = MaybeDropped::new(10); | ||
|
|
||
| unsafe { | ||
| // this is ok | ||
| to_be_dropped.assume_alive_drop(); | ||
| // this is not | ||
| // to_be_dropped.assume_alive_drop(); | ||
| }; | ||
|
|
||
| // this is not ok | ||
| // *ptr.cast_mut() = 42; | ||
| // this is not ok | ||
| // *ptr | ||
| ``` | ||
|
|
||
| ## Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| This is the definition | ||
|
|
||
| ``` | ||
| #[repr(transparent)] | ||
| pub struct MaybeDropped<T: ?Sized> { | ||
| value: MaybeUninit<ManuallyDrop<T>> | ||
| } | ||
| ``` | ||
|
|
||
| it does *not* store any drop-state information. | ||
|
|
||
| the public API will expose the following: | ||
|
|
||
| ``` | ||
| impl<T> MaybeDropped<T> { | ||
| pub unsafe fn assume_alive_drop(self); | ||
| pub unsafe fn assume_alive(self) -> T; | ||
| pub unsafe fn assume_alive_ref(&self) -> &T; | ||
| pub unsafe fn assume_alive_mut(&mut self) -> &mut Self; | ||
| pub unsafe fn as_ptr(&self) -> *const T; | ||
| pub unsafe fn as_mut_ptr(&mut self) -> *mut T; | ||
| } | ||
| ``` | ||
|
|
||
| ### Dropping | ||
|
|
||
| `MaybeDropped` will not drop on its own. | ||
|
|
||
| it must be dropped with `.assume_alive_drop()` | ||
|
|
||
| After drop: | ||
|
|
||
| - the `MaybeDropped`'s inner value is logically dropped. | ||
| - all access is undefined behaviour (both reads and writes) | ||
|
|
||
| ### Safety | ||
|
|
||
| All accesses to the inner value (outside of raw pointers) is `unsafe`, including references. | ||
|
|
||
| The inner value must be dropped *at most once*, not dropping is permitted but is considered a leak. | ||
|
|
||
| ### Interaction with Traits | ||
| `MaybeDropped` will not implement any traits which access the inner values (or, more generally, have any safe methods that access the wrapped value) | ||
|
|
||
| Trait implementations such as Send and Sync follow the same rules as `MaybeUninit<T>` and are conditional on the corresponding traits of `T`. | ||
|
|
||
| ### Relation with `MaybeUninit` | ||
|
|
||
| MaybeDropped<T> is closely related to mem::MaybeUninit<T>, but represents a distinct lifecycle. While MaybeUninit<T> models memory that may not yet have been initialized, MaybeDropped<T> models memory that was once initialized but may have been destroyed. | ||
|
|
||
| This distinction allows low-level code to express post-drop states explicitly without additional storage or ad-hoc flags. | ||
|
|
||
| ## Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| - It can already be done with `Option` | ||
| - It can already be done with `MaybeUninit` | ||
| - It is unnecessarily unsafe (in most cases) | ||
| - error prone | ||
| - if the drop-state tracking is not clear, users may accedeintly drop the wrapped value twice. | ||
|
|
||
| ## Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| - `Option<T>` | ||
| - Safer, | ||
| - Less error prone and | ||
| - tracks drop state | ||
|
|
||
| Option wasn't chosen because: | ||
|
|
||
| - it is not FFI safe | ||
| - it is not optimized for size | ||
|
|
||
| - `MaybeUninit` | ||
| - Already in code | ||
| - supported by external crates. | ||
|
|
||
| `MaybeUninit` wasn't chosen because: | ||
|
|
||
| - It represents a completely different lifecycle | ||
| - its methods dont align with the requirements of `MaybeDropped` | ||
|
|
||
| ### Impact of not doing this | ||
|
|
||
| there isnt much of an impact on regular code, the performance increase is negligble in regular code. | ||
|
|
||
| ## Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| `MaybeUninit` is already a way, inside of Rust, to represent data that may be invalid. | ||
|
|
||
| ### mem::MaybeUninit<T> | ||
|
|
||
| `mem::MaybeUninit<T>` is the closest existing abstraction to `MaybeDropped<T>`. It represents memory that may not yet be initialized as a valid T and provides no safe access to the underlying value. | ||
|
|
||
| While `MaybeUninit<T>` and `MaybeDropped<T>` share similar safety properties, they model different lifecycles. `MaybeUninit<T>` is intended for values that may be initialized at a later point, whereas `MaybeDropped<T>` models values that were once initialized but whose destructor may already have been executed. Conflating these lifecycles would obscure intent and make code harder to reason about. | ||
|
|
||
| ### mem::ManuallyDrop<T> | ||
|
|
||
| `mem::ManuallyDrop<T>` prevents Rust from automatically running the destructor of T while preserving all of T’s invariants. Safe access to the underlying value remains available at all times. | ||
|
|
||
| This makes `ManuallyDrop<T>` unsuitable for representing post-drop states, as it assumes the value remains valid even after the destructor is manually invoked. In contrast, `MaybeDropped<T>` explicitly removes any guarantee that the underlying value is valid. | ||
|
|
||
| ### Option<T> | ||
|
|
||
| `Option<T>` is commonly used to model early destruction by replacing a value with `None` once it is no longer needed. This approach is safe and idiomatic in many cases. | ||
|
|
||
| However, `Option<T>` introduces additional storage to track the presence of the value and semantically models optional ownership rather than post-drop invalidation. In cases where the drop state is already tracked elsewhere or where layout constraints matter, `Option<T>` is less suitable than `MaybeDropped<T>`. | ||
|
|
||
| ### Raw pointers and `drop_in_place` | ||
|
|
||
| Low-level Rust code can model post-drop states using raw pointers combined with ptr::drop_in_place. This approach is flexible but error-prone, as the drop state is tracked implicitly and must be enforced by convention. | ||
|
|
||
| MaybeDropped<T> encapsulates this pattern in a dedicated abstraction, making the intent explicit and reducing the likelihood of accidental misuse. | ||
|
|
||
| Additionally, also (usually) force borrowing rather than ownership (outside of `Box`) | ||
|
|
||
| ### Patterns in existing Rust code | ||
|
|
||
| Several low-level Rust abstractions internally rely on patterns similar to `MaybeDropped<T>`, including intrusive data structures, iterator adaptors, and runtime systems that perform early destruction for performance or correctness reasons. These implementations typically use `Option<T>`, `ManuallyDrop<T>`, or raw pointers to represent post-drop states. | ||
|
|
||
| `MaybeDropped<T>` provides a more direct and expressive way to model these patterns without additional storage or loss of clarity. | ||
|
|
||
| ### Other languages and systems | ||
|
|
||
| In systems programming contexts outside of Rust, it is common to distinguish between allocation, initialization, and destruction explicitly. Languages and runtimes such as C and C++ permit objects to be destroyed while their storage remains allocated, with correctness enforced by convention. | ||
|
|
||
| `MaybeDropped<T>` enables similar low-level control within Rust while preserving its safety model by confining such patterns to unsafe code. | ||
|
|
||
| ## Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| > - What parts of the design do you expect to resolve through the RFC process before this gets merged? | ||
|
|
||
| > - What parts of the design do you expect to resolve through the implementation of this feature before stabilization? | ||
| - Trait implementations | ||
| - For example, `Unpin`, `Send`, `Sync`? | ||
|
|
||
| ## Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| ### More general Lifetime Types | ||
|
|
||
| Currently, we support `uninit` -> `init` (and with this RFC, `init` -> `dropped`), but what if we want to add an abstraction layer over lifetimes as a whole? | ||
| Well, thats out of scope for this RFC, but it is a thought. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you can simply implement this whole type, assuming the implementation as you describe it here, as a simple wrapper around
MaybeUninit<T>. And with the changes toManuallyDropfrom RFC 3336 becoming available soon (AFAICT), eventually that can become a wrapper aroundManuallyDrop<T>. Types likeManuallyDrop<T>andMaybeUninit<T>are in the standard library because the former needs some compiler magic to be implemented, and the latter is used a lot and appears in argument or return types in a number of other standard library APIs. The standard library is deliberately kept somewhat minimalistic – I don’t see why theMaybeDropped<T>type here shouldn’t first onl[y be part of some downstream crate, where it can prove itself useful.Unless there is any fundamentally “magic” property that this
MaybeDroppedtype is supposed to have why it can not simply be implemented downstream, but I don’t see any mention of such a thing in this RFC.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely, parsing out the most I could get from the RFC, it feels like the proposal is just a misunderstanding of how
ManuallyDropworks, andManuallyDropwould work for the cases mentioned.And the actual proposed definition in the reference-level description is literally just
MaybeUninit<ManuallyDrop<T>>, so 🤷🏻There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do realize now I wrote this RFC in confusing wording (and i wrote like an LLM for some reason, i don't know why). Im also not a native english speaker, I will try to rewrite better and update it now.