nongraphical
nongraphical

Reputation: 25

Why doesn't &String automatically become &str in some cases?

In this toy example I'd like to map the items from a HashMap<String, String> with a helper function. There are two versions defined, one that takes arguments of the form &String and another with &str. Only the &String one compiles. I had thought that String always dereferences to &str but that doesn't seem to be the case here. What's the difference between a &String and a &str?

use std::collections::HashMap;

// &String works
fn process_item_1(key_value: (&String, &String)) -> String {
    let mut result = key_value.0.to_string();
    result.push_str(", ");
    result.push_str(key_value.1);
    result
}

// &str doesn't work (type mismatch in fn arguments)
fn process_item_2(key_value: (&str, &str)) -> String {
    let mut result = key_value.0.to_string();
    result.push_str(", ");
    result.push_str(key_value.1);
    result
}

fn main() {
    let mut map: HashMap<String, String> = HashMap::new();
    map.insert("a".to_string(), "b".to_string());

    for s in map.iter().map(process_item_2) {  // <-- compile error on this line
        println!("{}", s);
    }
}

Here's the error for reference:

error[E0631]: type mismatch in function arguments
  --> src/main.rs:23:29
   |
12 | fn process_item_2(key_value: (&str, &str)) -> String {
   | ---------------------------------------------------- found signature of `for<'r, 's> fn((&'r str, &'s str)) -> _`
...
23 |     for s in map.iter().map(process_item_2) {
   |                             ^^^^^^^^^^^^^^ expected signature of `fn((&String, &String)) -> _`

Thanks for your help with a beginner Rust question!

Upvotes: 1

Views: 215

Answers (1)

Chayim Friedman
Chayim Friedman

Reputation: 70830

It goes even stranger than that:

map.iter().map(|s| process_item_2(s)) // Does not work
map.iter().map(|(s1, s2)| process_item_2((s1, s2))) // Works

The point is that Rust never performs any expensive coercion. Converting &String to &str is cheap: you just take the data pointer and length. But converting (&String, &String) to (&str, &str) is no so cheap anymore: you have to take the data+length of the first string, then of the second string, then concatnate them together (and also, if it was done for tuple, what about (((&String, &String, &String), &String), (&String, &String))? And it was probably done then for arrays too, so what about &[&String; 10_000]?) That's why the first closure fails. The second closure, however, destruct the tuple and rebuild it. That means that instead of coercing a tuple, we coerce &String twice, and build a tuple from the results. That's fine.

The version without the closure is even more expensive: since you're passing a function directly to map(), and map produces &String, someone needs to convert this to &str! In order to do that, the compiler would have to introduce a shim - a small function that does that works: it takes (&String, &String) and calls process_item_2() with the (&String, &String) coerced to (&str, &str). This is a hidden cost, so Rust (almost) never creates shims. This is why it wouldn't work even for &String and not just for (&String, &String). And why |v| f(v) is not always the same as f - the first one performs coercions, while the second doesn't.

Upvotes: 3

Related Questions