1

I want to bound a type based on a function it can reach via deref coercions. Problem is bounding on Deref only works on one level of coercion.


struct A {}

impl A {
    fn foo(&self) {
        println!("foo");
    }
}

struct B {}

impl Deref for B {
    type Target = A;
    fn deref(&self) -> &Self::Target {
        &A{}
    }
}

struct C {}

impl Deref for C {
    type Target = B;
    fn deref(&self) -> &Self::Target {
        &B{}
    }
}

fn call_with_ref(obj: &A) {
    print!("call_with_ref: ");
    obj.foo();
}

fn checked_call_with_ref(obj: &A) {
    let ptr: *const A = obj;
    if !ptr.is_null() {
        print!("checked_call_with_ref: ");
        obj.foo();   
    } else {
        println!("checked_call_with_ref: null")
    }
}

fn call_with_pointer<T: Deref<Target = A>>(ptr: *const T) {
    if !ptr.is_null() {
        print!("call_with_pointer: ");
        unsafe { (*ptr).foo() };   
    }
}

fn main() {
    let c = C{};
    let ref_to_c = &c;
    let mut ptr_to_c: *const C = ref_to_c;
    ref_to_c.foo(); // works
    call_with_ref(ref_to_c); // works
    // call_with_pointer(ptr_to_C); // does not work bec. it derefs to B

    unsafe { checked_call_with_ref(&*ptr_to_c) }; // works on non-nulls
    ptr_to_c = std::ptr::null();
    unsafe { checked_call_with_ref(&*ptr_to_c) }; // undefined behavior on null pointers

    // have to put null check on each call site
    if let Some(ref_to_obj) = unsafe { ptr_to_c.as_ref() } {
        call_with_ref(ref_to_obj);
    }
}

Rust Playground

The foo function here is only implemented by A. Refs to B can call it with a direct coerce while refs to C can still call it via deref-ing to B. Deref<Target = A> is not the proper bound as C implements Deref<Target = B>. Does Rust have some sort of Coercible trait?

bob4433
  • 13
  • 3
  • 1
    Creating a reference from a null pointer is undefined behaviour, immediately, regardless of what you do with the reference. – Sven Marnach Oct 10 '19 at 18:29
  • Yes, didn't I indicate it as such? By "works", I mean it compiles. That code won't even print "checked_call_with_ref: null" if you run it on the playground. – bob4433 Oct 11 '19 at 03:12
  • Sure, you indicated that it is "potentially undefined behaviour", which I think is a bit weaker than "definitely undefined behaviour, regardless of what you do with the reference". (The comment wasn't necessarily meant for you, but rather for other people looking at this question in the future – you never know what kind of context will be useful for them.) – Sven Marnach Oct 11 '19 at 07:12
  • I see your point. I edited the question appropriately. – bob4433 Oct 11 '19 at 08:45

2 Answers2

2

Deref coercions are applied automatically by the compiler, so there is rarely any reason to make it an explicit bound. Just write call_foo to accept &A.

fn call_foo(obj: &A) {
    print!("Call from fn: ");
    obj.foo();
}

See also

trent
  • 25,033
  • 7
  • 51
  • 90
  • I'm dealing with potentially null pointers on my code. I edited my question to reflect this. I can either 1) do a null check on every call site of the function, violating DRY principle or 2) do a ptr -> ref -> ptr conversion, then check that last resulting pointer but I was told that this is [undefined behavior](https://www.reddit.com/r/rust/comments/deeoph/hey_rustaceans_got_an_easy_question_ask_here/f35wpof). – bob4433 Oct 10 '19 at 16:45
1

One solution is to introduce a helper trait that you recursively define for types that dereference to A:

use std::ops::Deref;

struct A;

impl A {
    fn foo(&self) {
        println!("foo");
    }
}

struct B;

impl Deref for B {
    type Target = A;
    fn deref(&self) -> &Self::Target {
        &A
    }
}

struct C;

impl Deref for C {
    type Target = B;
    fn deref(&self) -> &Self::Target {
        &B
    }
}

trait DerefA {
    fn get_a(&self) -> &A;
}

impl DerefA for A {
    fn get_a(&self) -> &A {
        self
    }
}

impl<T> DerefA for T
where
    T: Deref,
    T::Target: DerefA,
{
    fn get_a(&self) -> &A {
        (**self).get_a()
    }
}

fn glonk<T: DerefA>(p: *const T) {
    if p.is_null() {
        println!("Called with null pointer.");
    } else {
        let a = unsafe { &*p };
        a.get_a().foo();
    }
}

fn main() {
    let c = C;
    let ref_to_c = &c;
    let ptr_to_c: *const C = ref_to_c;
    glonk(ptr_to_c);
}

(Playground)

The helper trait DerefA is implemented for A itself, and then recursively for any type that dereferences to a type implementing DerefA. We don't get to use the compilers implicit dereferencing for our custom trait, so we need to explicitly call get_a() to derefence the whole chain to A.

I personally would be very reluctant to use something like this in real code, but it does what you asked for, and I also think it's a fun solution.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • If `A` itself implement `Deref` (no matter the target), you get conflicting implementations, which is an error. A slight limitation of this technique if you have no control over `A`. – WorldSEnder Mar 23 '22 at 02:34