The missing reference type: write-only

⚓ Rust    📅 2026-03-21    👤 surdeus    👁️ 1      

surdeus

I find myself wishing for a write-only reference type, and I'm curious to get feedback on this idea.

| read |       | &      | shared reference
| read | write | &mut   | mutable reference
|      | write | &write | write-only reference

The use case is for working with uninitialized data safely:

// STDLIB
impl<T, A> Vec<T, A> {
  /// returns the data between len and cap
  pub fn spare_capacity_write(&write self) -> &write [T] {
    ...
  }
}

// UPSTREAM CRATE: foo
/// fills some number n bytes of dst and returns n
pub fn decompress(src: &[u8], dst: &write [u8]) -> usize {
  ... // assign values to dst, but never read from it
}

// DOWNSTREAM CRATE: bar
fn main() {
  let (src0, src1) = ...;
  let mut dst = Vec::with_capacity(...);
  // write to uninitialized data:
  let bytes_written = decompress(&src0, dst.spare_capacity_write());
  unsafe { dst.set_len(bytes_written); }
  ... // do something with the now safely initialized dst
  // now reuse dst, writing to initialized data:
  decompress(&src1, dst.as_mut_slice()); // mut references can be cast to write
  ... // do something with dst again
}

In terms of borrow checking, &write would behave exactly the same as &mut. The difference is it would be impossible to read anything from the &write, allowing us to create safe methods like spare_capacity_write. In contrast, here's how to do this same thing today with MaybeUninit:

// STDLIB
impl<T, A> Vec<T, A> {
  /// returns the data between len and cap
  pub fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit<T>] {
    ...
  }
}

// UPSTREAM CRATE: foo
trait WriteableSlice<T> {
  fn into_maybe_uninit(self) -> &mut [MaybeUninit<T>];
}

impl<T> WriteableSlice<T> for &mut [T] {
  fn into_maybe_uninit(self) -> &mut [MaybeUninit<T>] {
    mem::transmute(self)
  }
}

impl<T> WriteableSlice<T> for &mut [MaybeUninit<T>] {
  fn into_maybe_uninit(self) -> &mut [MaybeUninit<T>] {
    self
  }
}

/// fills some number n bytes of dst and returns n
pub fn decompress<Dst: WriteableSlice<u8>>(src: &[u8], dst: Dst) -> usize {
  ... // assign values to dst, but never read from it
}

// DOWNSTREAM CRATE: bar
fn main() {
  let (src0, src1) = ...;
  let mut dst = Vec::with_capacity(...);
  // write to uninitialized data:
  let bytes_written = decompress(&src0, dst.spare_capacity_mut());
  unsafe { dst.set_len(bytes_written); }
  ... // do something with the now safely initialized dst
  // now reuse dst, writing to initialized data:
  decompress(&src1, dst.as_mut_slice()); // mut references can be cast to write
  ... // do something with dst again
}

Reasons I like this idea:

  • We no longer need to use traits and generics for the decompression API in foo to accept both initialized and uninitialized data.
  • The user in bar now has a simpler signature to work with.
  • It's now more guaranteed that we get a single compiled function in the assembly, rather than relying on optimization passes.
  • It's easier to assign a regular value (dst[i] = x) than a MaybeUninit (dst[i] = MaybeUninit::new(x)).

Of course, I recognize that adding a new type of reference would be a major undertaking, and I don't have a formal proposal for this. But I'm curious to hear what people think.

6 posts - 4 participants

Read full topic

🏷️ Rust_feed