Rust is the automatic dereference of Box types prohibative?

185 Views Asked by At

Method calls on Box objects in Rust automatically dereference the object contained in the Box wrapper.

For example

let my_variable: std::boxed::Box<i32> = std::boxed::Box::new(42);

let my_new_variable = my_variable.ilog2();

This example is contrived. The important concept here is that a function (in this case ilog2) can be called on some type (in this case i32) which is wrapped in a box.

In C++, the dereference and function call operator would be required:

// C++ would require this
my_new_variable = my_variable->ilog2();

My question is the following:

  • Does the automatic dereferencing of "boxed" objects prevent functions being called on the Box type itself? Are such function calls implicitly prohibited?

To further elaborate:

The type Box contains a function called leak.

  • How would one call the function leak() on a Box object if method calls implicitly dereference the contents of the box?
  • If calling a function which is associated with a Box object, on a Box object, "just works", then how would one disambiguate between two function calls which have the same name?(One associated with the Box and one associated with the type contained in the Box)

Syntactically:

If Box wraps type T such that we have Box<T>, and Box has a function Box::leak() and T has a function T::leak(), how do we disambiguate between the two function calls?

2

There are 2 best solutions below

6
user2722968 On

The details of this can be found in the reference on Method call expressions. The gist of it is the following

For instance, if the receiver has type Box<[i32;2]>, then the candidate types will be Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2] (by dereferencing), &[i32; 2], &mut [i32; 2], [i32] (by unsized coercion), &[i32], and finally &mut [i32].

That is, if Box<T> had a method .quack(), and T had a method of the same name, a method call like foo.quack() where foo is of type Box<T> would refer to the implementation Box::quack(); the T::quack() is effectively shadowed. As to your question, this does not prevent the Box-type (or any other Deref-type) from implementing methods that their inner type also implements, or vice versa.

For example:

struct Foo<T>(T);

impl<T> std::ops::Deref for Foo<T> {
    type Target = T;
    
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> Foo<T> {
    fn quack(&self) {
        println!("foo!");
    }
}

struct Bar;

impl Bar {
    fn quack(&self) {
        println!("bar!")
    }
}

fn main() {
    let foobar = Foo(Bar);
    // This will print "foo!".
    // Removing/renaming `Foo:quack` prints "bar!" without any change here
    foobar.quack();
}

In almost all cases, this behavior brings convenience to calling methods on any T that is wrapped in a smart-pointer. In order to visually/mentally disambiguate calls to methods on T vs. methods on the smart-pointer itself, smart-pointers usually don't have methods by themselves but only associated functions. Box::leak() is one example, and there are many others, which enforce a syntax like Box::leak(foo); instead of foo.leak(); for visual disambiguity.

One example where there is visual/mental ambiguity is the Clone implementation on Rc/Arc, where calling foo.clone() looks like cloning the inner T, while in fact, one is only cloning (increasing the reference count) of the outer Rc/Arc.

0
cafce25 On

In addition to using method syntax you can always fall back to the fully qualified names so even if Foo::quack is shadowing Bar::quack you can still use it, with the defnitions of this other answer:

fn main() {
    let foobar = Foo(Bar);
    // This will print "foo!".
    // Removing/renaming `Foo:quack` prints "bar!" without any change here
    foobar.quack();
    Foo::quack(&foobar); // will print "foo!" no matter what
    Bar::quack(&foobar); // will print "bar!" no matter what

}