-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: Introduce DerefInto and DerefMutInto for RAII access
#3880
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| - Feature Name: `deref_into` | ||
| - Start Date: 2025-11-10 | ||
| - RFC PR: [rust-lang/rfcs#3880](https://github.com/rust-lang/rfcs/pull/3880) | ||
| - Rust Issue: [rust-lang/rust#3880](https://github.com/rust-lang/rust/issues/3880) | ||
|
|
||
| # Summary | ||
| [summary]: #summary | ||
|
|
||
| Introduce supertraits `DerefInto` and `DerefMutInto` for `Deref` and `DerefMut` that return their targets by value with independent mutable and immutable targets. | ||
|
|
||
| This enables using deref coercion with RAII guards (`RefCell`, `Mutex`, `RwLock`...). | ||
|
|
||
| # Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| Rust's `Deref` and `DerefMut` traits are special because of the "Deref coercion". [Deref coercion is incredibly important for ergonomics. It's so that we can effectively interact with all of the different types of smart pointers as though they were regular ol' references e.g. allowing Box<T> where &T is expected.](https://www.reddit.com/r/rust/comments/1654y5l/comment/jyc9l4x). | ||
|
|
||
| However, the standard `Deref`/`DerefMut` traits are limited in that their output types are fixed references (`&Self::Target` / `&mut Self::Target`). | ||
|
|
||
| This makes them incompatible with interior mutability or synchronization types such as `RefCell`, `Mutex`, or `RwLock`, which return RAII guards by value (`Ref` / `RefMut` for `RefCell`, `MutexGuard` for `Mutex`, and `RwLockReadGuard` / `RwLockWriteGuard` for `RwLock`). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I think these "streaming deref" traits are useful, making the locks and cells implement them are very bad idea. The life time of the guards are significant, the behavior of the following 2 pieces of code are different: let sum_from_different_guards = { mutex.lock().unwrap().a } + { mutex.lock().unwrap().b };
let sum_from_same_guard = {
let guard = mutex.lock().unwrap();
guard.a + guard.b
};When you simplify it to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, it is very different. I expect the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
"Ambiguous" is too kind. It's clear Now, there are advanced algebra packages (for zero-knowledge proofs) that essentially do |
||
|
|
||
|
|
||
| ```rust | ||
| pub struct Foo | ||
| { | ||
| pub bar: i32, | ||
| } | ||
|
|
||
| let cell: RefCell<Foo> = RefCell::new(Foo{ bar: 42 }); | ||
|
|
||
| let _bar = cell.borrow().bar; // Current way to write it | ||
|
|
||
| let _bar = &cell.bar; // Error: Impossible to express right now, | ||
| // but more ergonomic to write | ||
| ``` | ||
|
Comment on lines
+23
to
+35
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't find this code more clear or ergonomic. Sure, okay, "ergonomic to write" maybe? But while it may be easier to write, it is much harder to read. We should not aspire to be a write-once language. |
||
|
|
||
| Currently, accessing fields through interior mutability types is verbose, requiring explicit calls to `.borrow()` / `.borrow_mut()` / `.lock()` / `.read()` / `.write()` instead of allowing a natural field/method access syntax. | ||
|
|
||
| # Guide-level explanation | ||
|
|
||
| Introduce the supertraits `DerefInto` and `DerefMutInto` (for `Deref` and `DerefMut`) to improve ergonomics in the core library: | ||
|
|
||
| ```rust | ||
| trait DerefInto | ||
| { | ||
| type Target<'out> where Self: 'out; | ||
| fn deref_into(&self) -> Self::Target<'_>; | ||
| } | ||
|
|
||
| trait DerefMutInto | ||
| { | ||
| type Target<'out> where Self: 'out; | ||
| fn deref_mut_into(&mut self) -> Self::Target<'_>; | ||
| } | ||
| ``` | ||
|
|
||
| ## Example for RefCell | ||
|
|
||
| ```rust | ||
| impl<T> DerefInto for RefCell<T> | ||
| { | ||
| type Target<'out> = std::cell::Ref<'out, T> where Self: 'out; | ||
| fn deref_into(&self) -> Self::Target<'_> | ||
| { | ||
| self.borrow() | ||
| } | ||
| } | ||
| impl<T> DerefMutInto for RefCell<T> | ||
| { | ||
| type Target<'out> = std::cell::RefMut<'out, T> where Self: 'out; | ||
| fn deref_mut_into(&mut self) -> Self::Target<'_> { | ||
| self.borrow_mut() | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| With these implementations, the following code becomes valid: | ||
|
|
||
| ```rust | ||
| let cell: RefCell<Foo> = RefCell::new(Foo{ bar: 42 }); | ||
| let bar = &cell.bar; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where is the let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let foo = &cell.foo;
let bar = &mut cell.bar; // `borrow_mut()` panics since we have a live `Ref` still
dbg!(foo);
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe borrowing the let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let sum = cell.foo_plus_bar(); // calling method
let x : i32 = cell.foo; // read field
cell.foo = 42; // write fieldAbout the storage behaviorOption 1: The temporary expires at the end of the statement:Since That would make it usable as an instantaneous reference usage where the RAII guard (like But it feels weird not being able to borrow it, which is why I prefer option 2: Option 2: Temporary +lifetime extension (+ different drop semantic):However, if its lifetime is extended and it lives until the end of the block, you can't manually drop the temporary t1 {
let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let foo = &cell.foo; // create a temporary t1: `Ref<'block,i32>` here
// + lifetime extend it, then deref t1 to take a
// reference to foo: `&'block i32` using the deref trait.
let bar = &mut cell.bar; // panics since t1 still exist
// t1 dropped here
}The issue is that the temporary Regular reference ( {
let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let foo = &cell.foo; // create a temporary t1: Ref<'block,i32> here
// + lifetime extend it, then deref t1 to take a
// reference to foo using the deref trait.
// t1 dropped here
let bar = &mut cell.bar; // don't panic since t1 is dropped
}So maybe types that behave like temporary references ( Something like that: trait NonLexicalDrop {}
impl<'a,T> NonLexicalDrop for Ref<'a,T> {}That way this code can compile and run fine: let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let foo = &cell.foo; // temporary reference t1 created here,
// temporary t1 dropped here
let bar = &mut cell.bar; // `borrow_mut()` work fine since t1 is droppedHowever, your code will indeed compile successfully, but it will panic at runtime due to improper use of the let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let foo = &cell.foo;
let bar = &mut cell.bar; // `borrow_mut()` panics since we have a live `Ref` still
dbg!(foo);Anyway, the support for the core library is secondary, I'm just using the Thank for your feedback :) Do you think I should update the RFC with this
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a non-lexical drop is too confusing when it actually needs to run code to drop the types (especially for locks), non-lexical lifetimes doesn't have this problem because dropping a reference doesn't actually do anything significant.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally I find non lexical drop more intuitive to think about than lexical one, but ok ^^. The way I understand non lexical drop is "drop it as soon as possible" (but maybe I'm wrong). // /!\ The comment indicate the status/name of the `y` variable
// lifetime if it was still active at the commented line.
let mut x = 42;
let y: &mut i32 = &mut x; // x borrow starts here: `Active` (lexical + non lexical)
println!("foo"); // `Active`
dbg!(y); // `Active
// `Extended`, because it is no longer used, but still here (lexical, and implementation detail for non lexical)
println!("bar"); // `Extended`
let z: &mut i32 = &mut x; // `Over Extended`: would cause a compile time error, even if it is no longer used (lexical only)P.S.: my pub trait Drop
{
const LIFETIME : DropLifetime = DropLifetime::Lexical;
fn drop(&mut self);
}
pub enum DropLifetime
{
Lexical,
NonLexical,
} |
||
| ``` | ||
|
|
||
| ## Example for RAII Singleton access | ||
|
|
||
| This approach also enables ergonomic RAII singleton access through zero-sized struct proxies, allowing calls like `MySingleton.foo()` or `MySingleton.foo_mut()` instead of requiring explicit singleton access methods such as `singleton().foo()` or `singleton_mut().foo_mut()`. | ||
|
|
||
| __Current way__: | ||
|
|
||
| ```rust | ||
| static SINGLETON: std::sync::RwLock<Foo> = RwLock::new(Foo { bar: 42 }); | ||
|
|
||
| // public API: | ||
| pub struct Foo { pub bar: i32 } | ||
|
|
||
| pub fn singleton<'a>() -> RwLockReadGuard<'a, Foo> | ||
| { | ||
| SINGLETON.read().unwrap() | ||
| } | ||
|
|
||
| pub fn singleton_mut<'a>() -> RwLockWriteGuard<'a, Foo> | ||
| { | ||
| SINGLETON.write().unwrap() | ||
| } | ||
|
|
||
| // Current usage: | ||
| let bar = singleton().bar; | ||
| singleton_mut().bar = 100; | ||
| ``` | ||
|
|
||
| __New way__: | ||
|
|
||
| ```rust | ||
| static SINGLETON: std::sync::RwLock<Foo> = RwLock::new(Foo { bar: 42 }); | ||
|
|
||
| // public API: | ||
| pub struct Foo { pub bar: i32 } | ||
|
|
||
| pub struct Singleton; | ||
|
|
||
| impl DerefInto for Singleton | ||
| { | ||
| type Target<'out> = RwLockReadGuard<'out, Foo>; | ||
| fn deref_into(&self) -> Self::Target<'_> { | ||
| SINGLETON.read().unwrap() | ||
| } | ||
| } | ||
| impl DerefMutInto for Singleton | ||
| { | ||
| type Target<'out> = RwLockWriteGuard<'out, Foo>; | ||
| fn deref_mut_into(&mut self) -> Self::Target<'_> { | ||
| SINGLETON.write().unwrap() | ||
| } | ||
| } | ||
|
|
||
| // New usage: | ||
| let bar = &Singleton.bar; | ||
| Singleton.bar = 100; | ||
| ``` | ||
|
|
||
| *Note:* With plain `Deref` / `DerefMut`, it is possible to create a zero-sized singleton proxy that forwards to a global value, but **RAII-based access is not possible**. | ||
|
|
||
| This is because `Deref` and `DerefMut` only allow returning **references** (`&T` / `&mut T`), which cannot carry the RAII lifetime of guards like `RefCell`'s Ref/RefMut or `RwLock`'s read/write guards. | ||
|
|
||
| - **Deref / DerefMut**: works for singleton proxies **if the value is just a reference**, but cannot safely return RAII guards. | ||
| - **DerefInto / DerefMutInto**: is required for RAII access, because they allow returning the **guard by value**, preserving the lifetime and safety of the borrow or lock. | ||
|
|
||
| # Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| This section introduces the supertraits `DerefInto` and `DerefMutInto` for `Deref` and `DerefMut` to improve ergonomics: | ||
|
|
||
| ```rust | ||
| trait DerefInto | ||
| { | ||
| type Target<'out> where Self: 'out; | ||
| fn deref_into(&self) -> Self::Target<'_>; | ||
| } | ||
|
|
||
| trait DerefMutInto | ||
| { | ||
| type Target<'out> where Self: 'out; | ||
| fn deref_mut_into(&mut self) -> Self::Target<'_>; | ||
| } | ||
| ``` | ||
|
|
||
| These traits provide **cheap, ergonomic access to RAII-guarded types** without explicit `.borrow()` or `.lock()` calls. | ||
| They are compatible with the standard `Deref`/`DerefMut` trait because they can return either a **RAII guard by value** or a **plain reference**, preserving the expected deref semantics. | ||
|
|
||
| ## Backward compatibility | ||
|
|
||
| These traits are intended to be the only traits that support Deref coercion. | ||
| The `Deref` and `DerefMut` trait implementation of the core library will just delegate to `DerefInto` and `DerefMutInto`: | ||
|
|
||
| ```rust | ||
| impl<T> DerefInto for T where T: Deref | ||
| { | ||
| type Target<'out> = &'out T::Target where Self: 'out; | ||
| fn deref_into(&self) -> Self::Target<'_> { | ||
| self.deref() | ||
| } | ||
| } | ||
| impl<T> DerefMutInto for T where T: DerefMut | ||
| { | ||
| type Target<'out> = &'out T::Target where Self: 'out; | ||
| fn deref_mut_into(&mut self) -> Self::Target<'_> { | ||
| self.deref_mut() | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| This ensures full backward compatibility, while enabling ergonomic, by-value RAII access for interior mutability or synchronization types. | ||
|
|
||
| # Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| This adds complexity to the `Deref` and `DerefMut` APIs, especially since the `DerefInto` and `DerefMutInto` Target types may differ and support the deref coercion feature. | ||
|
|
||
| Implementing them on `Mutex` or `RwLock` may be undesirable because: | ||
|
|
||
| - The `.lock()` / `.read()` / `.write()` can fail, so the `DerefInto`/`DerefMutInto` implementation may panic. (The `Index`/`IndexMut` trait work in a similar way, panic on invalid indices via [`index()`](https://doc.rust-lang.org/std/ops/trait.Index.html#tymethod.index)). | ||
|
|
||
| - Most notably, if `DerefInto` and `DerefMutInto` are implemented for `Mutex` or `RwLock`, it's not clear how these trait should behave on poisoned state. | ||
|
Comment on lines
+199
to
+203
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another disadvantage for
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point. It can make the syntax more ergonomic (especially for experienced programmers), but it also makes the code more complex to reason about and can provoque more accidental race conditions. I wonder if there is a way to detect such a things in "trivial" situation with compile-time warnings. "Hey, I take a reference eg, the pub fn borrow_mut<'a>(s: &'a RefCell<T>) -> RefMut<'a, T> + &'a mut
{
...
}
pub fn borrow<'a>(s: &'a RefCell<T>) -> Ref<'a, T>
{
...
}So, situations like this can trigger a borrow error at compile time (for free, thanks to the existing borrow checker?): let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let x = cell.borrow_mut(); // take a &self, but return the compiler is aware that
// this is a &mut self borrow. (&self -> &mut Self)
let y = cell.borrow(); // error: cannot borrow `cell` as immutable because it is also borrowed as mutable
dbg!(x.bar);And maybe the same kind of error could be detected for: let cell = RefCell::new(MyStruct { foo: 1, bar: 2});
let y = cell.borrow(); // &mut self borrow
let x = cell.borrow_mut(); // error: cannot borrow `cell` as mutable because it is also borrowed as mutable
dbg!(y.bar);So maybe these problems could be detected at compile time (which would be even better than now, since they are currently detected only at runtime)? (I haven’t dug deeper; there may be more problem, but the code that will handle the |
||
|
|
||
| # Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| RAII access by value is only possible using `DerefInto` / `DerefMutInto`, not with standard `Deref` / `DerefMut`. | ||
|
|
||
| This design extends the existing `Deref` / `DerefMut` trait, preserving their semantics while generalizing their output type to support by-value dereferencing. | ||
|
|
||
| It also makes the language more expressive and ergonomic while also preserving performance and safety, and backward compatibility. | ||
|
|
||
| This feature cannot be implemented purely as a library or macro, since Deref coercion is a compiler-level behavior. Extending it to support by-value outputs requires language support. | ||
|
|
||
| I hope that a mechanism similar to the proposed `DerefInto` and `DerefMutInto` will be available initially. | ||
|
|
||
| The implementation of these traits for existing types such as `RefCell`, `Mutex`, and `RwLock` could be addressed later, rejected or not, or considered out of scope for this initial RFC. | ||
|
|
||
| Since this feature primarily benefits user-defined types and library authors seeking more ergonomic access patterns, native support in the core library can reasonably come at a later stage once the mechanism is stable. | ||
|
|
||
| # Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| - `DerefMut` implies `Deref`, but can `DerefMutInto` be implemented without `DerefInto` ? | ||
|
|
||
| - If the `DerefInto` implementation is pure, does that mean the target type is clonable? | ||
|
|
||
| - If both traits are implemented and their target types differ, how should name resolution work? Should the resolver first look into the `DerefInto` target for the field or method, and then fall back to the `DerefMutInto` target if not found? | ||
|
|
||
| - Remove the reference in the parameter to make it more flexible ? | ||
| ex: | ||
| ```rust | ||
| trait DerefIntoByValue | ||
| { | ||
| type Target; | ||
| fn deref_into(self) -> Self::Target; | ||
| } | ||
| ``` | ||
| In that case: | ||
|
|
||
| - There is no need for the `DerefMutIntoByValue` trait because `DerefIntoByValue` can be implemented for `&mut T`. | ||
|
|
||
| - The `DerefIntoByValue` trait look really similar to `Into`, except it is not generic and use a GAT. Does `DerefIntoByValue` imply `Into<Self::Target>`? | ||
|
|
||
| # Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| No additional future possibilities are identified at this time. | ||
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 think this is desirable and in fact that we need to go further and combine this with the ability to project to subvalues. I was just working on a proposal like this in the context of the Field Projections initiative: https://hackmd.io/N0sjLdl1S6C58UddR7Po5g .
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 agee, this is helpful, and it can help solving some problem with
DerefandDerefMut.Personally I'm more interested in partial borrows or view types like described in the blog you cited by Niko Matsakis because the syntax is closer to destructuring.
I hope that partial borrows/moves and view types will eventually follow a similar syntax to destructuring, and that it will be compositional:
Especially if:
&selfborrow the whole struct,&mut selfborrow the whole struct as mutable,selfmove the struct,It would be great if we can be more explicit about how each field is reference or moved.
For example, given
struct Foo { bar: i32, buz: i32 }:(
<=>mean equivalent)&self<=>self { &bar, &buz }(distribute the reference)&mut self<=>self { &mut bar, &mut buz }<=>&self { mut bar, mut buz }self<=>self { bar, buz }Combinaison like
self { &mut bar, &buz }<=>&self { mut bar, buz }should be possible(borrow
baras a mutably, andbuzimmutably).Unlisted fields would not be used (neither referenced nor moved):
self { &bar, buz: _ }<=>self { &bar }And a way to qualify all the unused fields could be to use the struct update syntax:
&self<=>self { &bar, &buz }<=>self { &bar, &.. }<=>self { &.. }<=>&self { .. }That way, implementations of
Derefcan be more specific and borrow only a subset of the fields.Even though this design is not generic-compatible, abstract field solve it
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.
Partial borrows/view types are compatible and orthogonal to place-based field projections I believe. Place-based field projections just extend all the borrowck things from references to custom smart pointers.