Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions text/3880-deref_into.md
Copy link
Member

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 .

Copy link
Author

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 Deref and DerefMut.

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:

  • &self borrow the whole struct,
  • &mut self borrow the whole struct as mutable,
  • and self move 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 bar as a mutably, and buz immutably).

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 Deref can be more specific and borrow only a subset of the fields.

struct Foo
{
    pub bar: Bar,
    pub value_independant_to_bar: i32,
}
struct Bar;

impl Deref for Foo
{
    type Target=Bar;
    // here:
    fn deref(&self { bar }) -> &Self::Target {
        &self.bar
    }
}

Even though this design is not generic-compatible, abstract field solve it

Copy link
Member

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.

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`).
Copy link
Member

Choose a reason for hiding this comment

The 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 mutex.a + mutex.b, it is ambiguous which locking pattern do you expect

Copy link
Author

@Thomas-Mewily Thomas-Mewily Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it is very different.

I expect the sum_from_different_guards locking pattern to be used for mutex.a + mutex.b, but that also means it will be locked twice for the same operation (which is undesirable). Perhaps the sum_from_same_guard pattern can be used if the DerefInto trait is pure/idempotent.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you simplify it to mutex.a + mutex.b, it is ambiguous which locking pattern do you expect

"Ambiguous" is too kind. It's clear mutex.a + mutex.b always means 'sum_from_different_guards', so that's almost always the wong choic, since it cases a race condition. It's unworkable for locking and guards.

Now, there are advanced algebra packages (for zero-knowledge proofs) that essentially do impl Add<Thunk> for Thunk { type Ouput = Thunk; .. } and the Thunk type (aka contraint system) contains some Arc<Mutex<..>> which this Add::add transforms. It's possible these trait help there somehow, but having them for Mutex itself can only cause harm.



```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
Copy link
Member

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the Ref<'out, T> stored so you can borrow from it? if it's a temporary like in &cell.borrow().bar, you can't actually store the returned reference with let bar since the lifetime expires at the end of the statement when the Ref<'out, T> is dropped. If you say it has its lifetime extended and is dropped at the end of the block, imo that makes it ill-suited for RefCell and other locking-style types since you can't drop the Ref before you need to access the RefCell again for borrow_mut() or similar:

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);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe borrowing the .foo field (implying you will use it later) wasn't the best example, but there are also other practical use cases such as:

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 field

About the storage behavior

Option 1: The temporary expires at the end of the statement:

Since deref_into() is still a method, I would assume it produce a temporary value in the expression that expires at the end of the statement.

That would make it usable as an instantaneous reference usage where the RAII guard (like Ref<'stmt, T>) lives only for the duration of the statement (like in the code above).
If you tried to store a reference you'd get a compiler error like "cannot borrow temporary value as it does not live long enough"

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 Ref before the end of the block, as you noted. Therefore, the following code would not work:

{
  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 t1 : Ref<'block,i32> act like a reference wrapper with some RAII, but it follow the same lexical drop semantic (like a struct/enum) : it is dropped at the end of the block.

Regular reference (&T / &mut T) instead follow non-lexical lifetimes. If this temporary t1 behaved the same way, the following code would work:

{
  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 ( Ref / RefMut / MutexGuard / RwLockReadGuard / RwLockWriteGuard... i.e.: that need to be dropped early to avoid holding a borrow) should be marked with a special compiler trait marker to follow non lexical lifetime ?

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 dropped

However, your code will indeed compile successfully, but it will panic at runtime due to improper use of the RefCell's borrowed value (a programmer error in that case I guess?).

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 RefCell as an example, but it could be any custom user defined struct or enum.

Thank for your feedback :)

Do you think I should update the RFC with this NonLexicalDrop trait for reference like type now, or keep discussing it a bit more first?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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).
So they are ideal for releasing resource as soon as possible.

  // /!\ 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 NonLexicalDrop trait suggestion may be replaced by a constant enum in the Drop trait to define when to drop it (but it makes it unusable in a generic constraint).

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another disadvantage for Defef(Mut)Into for Mutex and RwLock might be that it’s less clear when one of these is locked or unlocked, possibly leading to race conditions when some code that should unlock and lock only once accidentally does so multiple times.

Copy link
Author

@Thomas-Mewily Thomas-Mewily Nov 11, 2025

Choose a reason for hiding this comment

The 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.
This sounds much more complex and beyond the scope of this RFC. It would probably require introducing a new rule or syntax to tell the compiler:

"Hey, I take a reference &'a T in parameter, but the output type will safely have borrowed mutable reference &'a mut T."

eg, the + &'a mut in:

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 fn (&self) -> &mut Self will be unsafe)
Thank for the answer btw!


# 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.