Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does adding a second impl prevent deref coercion of the argument?

I came across this issue when trying to add the impl Add<char> for String to the standard library. But we can replicate it easily, without operator shenanigans. We start with this:

trait MyAdd<Rhs> {
    fn add(self, rhs: Rhs) -> Self;
}

impl MyAdd<&str> for String {
    fn add(mut self, rhs: &str) -> Self {
        self.push_str(rhs);
        self
    }
}

Simple enough. With this, the following code compiles:

let a = String::from("a");
let b = String::from("b");
MyAdd::add(a, &b);

Note that in this case, the second argument expression (&b) has the type &String. It is then deref-coerced into &str and the function call works.

However, let's try to add the following impl:

impl MyAdd<char> for String {
    fn add(mut self, rhs: char) -> Self {
        self.push(rhs);
        self
    }
}

(Everything on Playground)

Now the MyAdd::add(a, &b) expression above leads to the following error:

error[E0277]: the trait bound `std::string::String: MyAdd<&std::string::String>` is not satisfied
  --> src/main.rs:24:5
   |
2  |     fn add(self, rhs: Rhs) -> Self;
   |     ------------------------------- required by `MyAdd::add`
...
24 |     MyAdd::add(a, &b);
   |     ^^^^^^^^^^ the trait `MyAdd<&std::string::String>` is not implemented for `std::string::String`
   |
   = help: the following implementations were found:
             <std::string::String as MyAdd<&str>>
             <std::string::String as MyAdd<char>>

Why is that? To me it seems like deref-coercion is only done when there is only one function candidate. But this seems wrong to me. Why would the rules be like that? I tried looking through the specification, but I haven't found anything on argument deref coercion.

like image 433
Lukas Kalbertodt Avatar asked Nov 16 '19 10:11

Lukas Kalbertodt


People also ask

What is deref coercion?

This mechanism is called ‘ Deref coercion’. In mutable contexts, DerefMut is used. Implementing Deref for smart pointers makes accessing the data behind them convenient, which is why they implement Deref. On the other hand, the rules regarding Deref and DerefMut were designed specifically to accommodate smart pointers.

Why do we implement deref for smart pointers?

Implementing Deref for smart pointers makes accessing the data behind them convenient, which is why they implement Deref. On the other hand, the rules regarding Deref and DerefMut were designed specifically to accommodate smart pointers. Because of this, Deref should only be implemented for smart pointers to avoid confusion.

What is the difference between deref and derefmut?

In mutable contexts, DerefMut is used. Implementing Deref for smart pointers makes accessing the data behind them convenient, which is why they implement Deref. On the other hand, the rules regarding Deref and DerefMut were designed specifically to accommodate smart pointers.


1 Answers

As you yourself explained, the compiler treats the case where there is only one valid impl specially, and can use this to drive type inference:

Here is a comment which states that if only one impl is found the compiler "eagerly confirms" it, which allows deref coercions (among other things) to happen. That does not happen for multiple impl candidates.

The second part is that deref coercion will only happen at sites where the expected type is known, it doesn't happen speculatively. See coercion sites in the reference. Impl selection and type inference must first explicitly find that MyAdd::add(&str) would be expected, to attempt to coerce the argument to &str.

If a workaround is needed in this situation, use an expression like &*b or &b[..] or b.as_str() for the second argument.

like image 187
ramslök Avatar answered Oct 22 '22 07:10

ramslök