This semi-surprising corner case came up in some recent Rust training I was giving. I figured a short write-up may help some others in the future.
Rust's language design focuses on ergonomics. The goal is to make common patterns easy to write on a regular basis. This overall works out very well. But occasionally, you end up with a surprising outcome. And I think this situation is a good example.
Let's start off by pretending that method syntax doesn't exist at all. Let's say I've got a String
, and I want to clone it. I know that there's a Clone::clone
method, which takes a &String
and returns a String
. We can leverage that like so:
fn uses_string(x: String) {
println!("I consumed the String! {}", x);
}
fn main() {
let name = "Alice".to_owned();
let name_clone = Clone::clone(&name);
uses_string(name);
uses_string(name_clone);
}
Notice that I needed to pass &name
to clone
, not simply name
. If I did the latter, I would end up with a type error:
error[E0308]: mismatched types
--> src\main.rs:7:35
|
7 | let name_clone = Clone::clone(name);
| ^^^^
| |
| expected reference, found struct `String`
| help: consider borrowing here: `&name`
And that's because Rust won't automatically borrow a reference from function arguments. You need to explicit say that you want to borrow the value. Cool.
But now I've remembered that method syntax is, in fact, a thing. So let's go ahead and use it!
let name_clone = (&name).clone();
Remembering that clone
takes a &String
and not a String
, I've gone ahead and helpfully borrowed from name
before calling the clone
method. And I needed to wrap up that whole expression in parentheses, otherwise it will be parsed incorrectly by the compiler.
That all works, but it's clearly not the way we want to write code in general. Instead, we'd like to forgo the parentheses and the &
symbol. And fortunately, we can! Most Rustaceans early on learn that you can simply do this:
let name_clone = name.clone();
In other words, when we use method syntax, we can call .clone()
on either a String
or a &String
. That's because with a method call expression, "the receiver may be automatically dereferenced or borrowed in order to call a method." Essentially, the compiler follows these steps:
- What's the type of
name
? OK, it's a String
- Is there a method available that takes a
String
as the receiver? Nope.
- OK, try borrowing it. Is there a method available that takes a
&String
as the receiver? Yes. Use that!
And, for the most part, this works exactly as you'd expect. Until it doesn't. Let's start off with a confusing error message. Let's say I've got a helper function to loudly clone a String
:
fn clone_loudly(x: &String) -> String {
println!("Cloning {}", x);
x.clone()
}
fn uses_string(x: String) {
println!("I consumed the String! {}", x);
}
fn main() {
let name = "Alice".to_owned();
let name_clone = clone_loudly(&name);
uses_string(name);
uses_string(name_clone);
}
Looking at clone_loudly
, I realize that I can easily generalize this to more than just a String
. The only two requirements are that the type must implement Display
(for the println!
call) and Clone
. Let's go ahead and implement that, accidentally forgetting about the Clone
:
use std::fmt::Display;
fn clone_loudly<T: Display>(x: &T) -> T {
println!("Cloning {}", x);
x.clone()
}
As you'd expect, this doesn't compile. However, the error message given may be surprising. If you're like me, you were probably expecting an error message about missing a Clone
bound on T
. In fact, we get something else entirely:
error[E0308]: mismatched types
--> src\main.rs:4:5
|
2 | fn clone_loudly<T: Display>(x: &T) -> T {
| - this type parameter - expected `T` because of return type
3 | println!("Cloning {}", x);
4 | x.clone()
| ^^^^^^^^^ expected type parameter `T`, found `&T`
|
= note: expected type parameter `T`
found reference `&T`
Strangely enough, the .clone()
seems to have succeeded, but returned a &T
instead of a T
. That's because the method call expression is following the same steps as above with String
, namely:
- What's the type of
x
? OK, it's a &T
- Is there a
clone
method available that takes a &T
as the receiver? Nope, since we don't know that T
implements the Clone
trait.
- OK, try borrowing it. Is there a method available that takes a
&&T
as the receiver? Interestingly yes.
Let's dig in on that Clone
implementation a bit. Removing a bit of noise so we can focus on the important bits:
impl<T> Clone for &T {
fn clone(self: &&T) -> &T {
*self
}
}
Since references are Copy
able, derefing a reference to a reference results in copying the inner reference value. What I find fascinating, and slightly concerning, is that we have two orthogonal features in the language:
- Method call syntax automatically causing borrows
- The ability to implement traits for both a type and a reference to that type
When combined, there's some level of ambiguity about which trait implementation will end up being used.
In this example, we're fortunate that the code didn't compile. We ended up with nothing more than a confusing error message. I haven't yet run into a real life issue where this behavior can result in code which compiles but does the wrong thing. It's certainly theoretically possible, but seems unlikely to occur unintentionally. That said, if anyone has been bitten by this, I'd be very interested to hear the details.
So the takeaway: autoborrowing and derefing as part of method call syntax is a great feature of the language. It would be a major pain to use Rust without it. I'm glad it's present. Having traits implemented for references is a great feature, and I wouldn't want to use the language without it.
But every once in a while, these two things bite us. Caveat emptor.
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.
Do you like this blog post and need help with Next Generation Software Engineering, Platform Engineering or Blockchain & Smart Contracts? Contact us.