2

It makes sense to forbid implicit dereferencing for non-trivial types (that implement drop). But I'm curious what the reasoning is behind extending this prevention to primitives as well.

Since primitives are copied and not moved, wouldn't it make sense to provide implicit dereferencing for primitive types?

Is there a situation where implicit dereferencing of primitive types could be dangerous? Or is this just something that the rust compiler isn't able to do yet?

Semin Park
  • 650
  • 6
  • 20
  • 1
    I think this is just a design decision. What would indexing by a reference to a number *mean*? – Aleksander Krauze Oct 22 '22 at 10:58
  • 1
    Is the question just, why doesn't the compiler implicitly treat `arr[idx]` as `arr[*idx]` in this code? In other contexts the `*` is not implicit, e.g. calling a function with an argument of type `&i32` when the parameter type is `i32`, will give a similar error. So unless there is some specific reason you think vector indexing should be different, perhaps your question should be why references aren't implicitly dereferenced in general. – kaya3 Oct 22 '22 at 11:02
  • 1
    @kaya3 There are many contexts where `*` is implicit, such as [method calls](https://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rules). In other situations the stdlib ensures that references "just work" e.g. `&1 < &2` compiles and means the same as `1 < 2`. An argument could be made that indexing slices by `&usize` could automatically dereference, and that that would be unambiguous, since indexing by reference has no other meaning. I suspect this wasn't considered because it would interfere with the more general `Index` trait. – user4815162342 Oct 22 '22 at 11:20
  • @kaya3 New rustean here.. yeah after posting this question I realized that you can't pass a reference to a primitive into a function which expects the type without reference. I think I should rather ask why references aren't implicitly dereferenced specifically in case of primitive types. – Semin Park Oct 22 '22 at 11:31
  • @user4815162342: Indeed, it `std::cmp::PartialOrd::lt` takes the Rhs by reference, so in `&1 < &2` adding an implicit `*` is compensated by another implicit `&` (`&*&2`). But `std::ops::Index::index` takes the Idx by value, adding an implicit `*` would change the semantics of the call in the general case, if Idx does not implement `Copy`. – rodrigo Oct 22 '22 at 11:31
  • @SeminPark Upon re-reading my comment I think it may not have come across as I intended - sorry for that. I didn't mean you should be expected to know this already, rather what I meant was perhaps this is new information and therefore you might want to change your question in light of it, but perhaps there is also a reason you might only expect this for vector indexing in which case that's worth clarifying. (I'm also fairly new to Rust, as it happens.) – kaya3 Oct 22 '22 at 11:42
  • @kaya3 No problem, I understood your original comment, but didn't have time to edit my question earlier. Hope this edit addresses the question better :) – Semin Park Oct 22 '22 at 13:42

1 Answers1

4

Rust has a lot of operations that work with both references and objects. These include things like the comparison operators (x < y is the same as &x < &y), method calls ((&x).func() is the same as x.func()), and certain functions (println!("{}", &x) is the same as println!("{}", x)). However, there are only three different ways in Rust for references to be treated the same as objects.

The first way is trait implementations. Some traits are implemented for a type T, but they also include a blanket implementation impl<T> Trait for &T that just defers to the Trait implementation for T. One example of this is the standard comparison operators, which use the std::cmp::Ord trait. This trait has a blanket implementation, impl<A> Ord for &A, which just defers to the Ord implementation on A. This is why you can compare two i32s (since i32 implements Ord), and you can compare two &i32s (since &i32 implements Ord because of the blanket implementation), but you can't compare an i32 and &i32 (since they're different types).

The second way is the dot operator (.). When you call a method on a value, (&3).abs(), Rust does a whole bunch of magic to automatically coerce the type into one that applies to the method. The full specification is here, but the important part is that Rust will try to implicitly dereference the value if it doesn't detect a valid method call on a reference. Rust even does this recursively, meaning that you can write (&&&&&&&&&&&3).abs() and still have it compile. This is why method calls can take references along with objects.

The third way is the std::borrow::Borrow trait. Borrow<T> is satisfied by both T and &T, meaning that certain functions can state that they will accept either references or objects. The following code compiles and prints your number is 3 twice:

use std::borrow::Borrow;

fn f<T: Borrow<i32>>(num: T) {
    println!("your number is {}", num.borrow());
}

fn main() {
    f(3);
    f(&3);
}

The reason why vector[&idx] doesn't magically dereference the argument is because the [] indexing operator is handled by the std::ops::Index trait, and the index function from that trait only accepts the exact type. The std::ops::Index::index function could be changed to use Borrow, but for all the Rust devs know, someone might want to implement a data structure that handles data[&x] and data[x] independently, and they want to facilitate that.

So why doesn't Rust do this dereferencing for primitive types implicitly?

Rust attempts to treat primitive types the exact same as user-provided types: they don't have any extra special behavior that can't be recreated on a user-provided type. This is why crates like ux can provide more 'primitive' types like i7, u4 and i42: because there's nothing that primitive types can do that user-defined types can't do as well.

Since we'd want to extend this auto-dereference behavior to user-provided types, we might go ahead and say "okay, all Copy types will have auto-dereference behaviour." The problem is that types like [u8; 1024*1024*1024*1024] (i.e. a 1GB sized type) implement Copy, and you definitely don't want to autodereference them (imagine messing up in your code and accidentally creating a 1GB-sized copy... yikes.) So then you might say that "okay, let's make a trait AutoDeref that you #derive to get autoderefence behaviour." Now the other problem comes into play: this would increase compile time and compiler complexity significantly, as Rust now needs to dereference juggle all primitive operations in any context.

TL;DR: It would increase compiler complexity and compile time significantly.

virchau13
  • 1,175
  • 7
  • 19