Smart mutable/immutable reference using PhantomData?


#1

Hi rustaceans,

I’m trying to write smart references which follows the lifetime borrowing rules:

  • A reference cannot outlive its referent
  • A mutable reference cannot be aliased

The snippet below is how to implement a smart pointer that honors the same behavior to &mut T, regardless if the callee is marked as mutable or not.

struct SmartPtr<'a, T: 'a> {
    pointer: *mut T,
    _marker: PhantomData<&'a mut T>,
}

#![derive(Debug)]
struct Container<T> {
    arr: [T; 3]
}

impl <T> Container<T> {
    fn get_smart_ptr(&mut self, idx: usize) -> SmartPtr<T> {
        SmartPtr {
            pointer: &mut self.arr[idx] as *mut T,
            _marker: PhantomData,
        }
    }
}

fn main() {
    let mut c = Container{arr: [1, 2, 3]};
    let s_ptr = c.get_smart_ptr(1);
    // s_ptr is treated as `&mut T` because the lifetime is hard coded in the Phantom type
    println!("{:?}", c); // desired compilation fail: cannot borrow `c` as immutable because it is also borrowed as mutable
    
    // if we wrote the above as:
    {
        let s_ptr = c.get_smart_ptr(1);
    }
    println!("{:?}", c); // now the scoping is correct and it compiles

The problem is, the smart reference/pointer is hard coded with its mutability signature. Is it possible to create a smart pointer such that we can use it as & T or &mut T depends on its context?

For instance, the ideal case would be:

let x = c.get_smart_ptr(); // immutable reference, as if it was &T
let mut x_mut = c.get_smart_ptr(); // mutable reference, as if it was &mut T

Less ideal, but still acceptable:

let x: SmartPtr<&T> = c.get_smart_ptr();
let x_mut: SmartPtr<&mut T> = c.get_smart_ptr_mut();
// Is reference in generic valid?

In this case, SmartPtr can use the generics to infer if it is a mutable reference or not.

Doable, but hopefully we have a better solution:

let x: SmartPtr<T> = c.get_smart_ptr();
let x_mut: SmartPtrMut<T> = c.get_smart_ptr_mut();

This is the most trivial implementation. We can define two types that has exactly the same layout, only the lifetime signature in the PhantomData is different.

Any thought on how to implement it with idiomatic rust?
Thanks.


#2

For anyone who have the similar issue, I figured out how to create smart references honers the borrowing rules.

use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};

struct MyPtr<T, U> {
    pointer: *mut T,
    _marker: PhantomData<U>,
}

#[derive(Debug)]
struct Container<T> {
    data: [T; 3]
}

impl<T> Container<T> {
    fn get_ptr_mut(&mut self, idx: usize) -> MyPtr<T, &mut T> {
        MyPtr {
            pointer: &mut self.data[idx] as *mut T,
            _marker: PhantomData
        }
    }
    fn get_ptr(&self, idx: usize) -> MyPtr<T, &T> {
        MyPtr {
            pointer: &self.data[idx] as *const T as *mut T,
            _marker: PhantomData
        }
    }
}

impl <'a, T> Deref for MyPtr<T, &'a T> {
    type Target = T;
    fn deref(&self) -> &T {
        unsafe { &*self.pointer }
    }
}

impl <'a, T> Deref for MyPtr<T, &'a mut T> {
    type Target = T;
    fn deref(&self) -> &T {
        unsafe { &mut *self.pointer }
    }
}

impl <'a, T> DerefMut for MyPtr<T, &'a mut T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { &mut *self.pointer }
    }
}

fn main() {
    let mut c = Container{data: [1, 2, 3]};

// mutable references must be used in its own scope
// cannot share mutable reference and immutable references in the same scope
    {
        let mut m_ref = c.get_ptr_mut(0);
        *m_ref = 4;
    }
// We can have multiple immutable references pointing to the same object in the same scope
    let i_ref = c.get_ptr(1);
    println!("i_ref: {}", *i_ref);
    println!("c: {:?}", c);
}

Output:

i_ref: 2
c: Container { data: [4, 2, 3] }