Why is it discouraged to accept a reference to a String (&String), Vec (&Vec), or Box (&Box) as a function argument?

16,864

Solution 1

TL;DR: One can instead use &str, &[T] or &T to allow for more generic code.


  1. One of the main reasons to use a String or a Vec is because they allow increasing or decreasing the capacity. However, when you accept an immutable reference, you cannot use any of those interesting methods on the Vec or String.

  2. Accepting a &String, &Vec or &Box also requires the argument to be allocated on the heap before you can call the function. Accepting a &str allows a string literal (saved in the program data) and accepting a &[T] or &T allows a stack-allocated array or variable. Unnecessary allocation is a performance loss. This is usually exposed right away when you try to call these methods in a test or a main method:

    awesome_greeting(&String::from("Anna"));
    
    total_price(&vec![42, 13, 1337])
    
    is_even(&Box::new(42))
    
  3. Another performance consideration is that &String, &Vec and &Box introduce an unnecessary layer of indirection as you have to dereference the &String to get a String and then perform a second dereference to end up at &str.

Instead, you should accept a string slice (&str), a slice (&[T]), or just a reference (&T). A &String, &Vec<T> or &Box<T> will be automatically coerced (via deref coercion) to a &str, &[T] or &T, respectively.

fn awesome_greeting(name: &str) {
    println!("Wow, you are awesome, {}!", name);
}
fn total_price(prices: &[i32]) -> i32 {
    prices.iter().sum()
}
fn is_even(value: &i32) -> bool {
    *value % 2 == 0
}

Now you can call these methods with a broader set of types. For example, awesome_greeting can be called with a string literal ("Anna") or an allocated String. total_price can be called with a reference to an array (&[1, 2, 3]) or an allocated Vec.


If you'd like to add or remove items from the String or Vec<T>, you can take a mutable reference (&mut String or &mut Vec<T>):

fn add_greeting_target(greeting: &mut String) {
    greeting.push_str("world!");
}
fn add_candy_prices(prices: &mut Vec<i32>) {
    prices.push(5);
    prices.push(25);
}

Specifically for slices, you can also accept a &mut [T] or &mut str. This allows you to mutate a specific value inside the slice, but you cannot change the number of items inside the slice (which means it's very restricted for strings):

fn reset_first_price(prices: &mut [i32]) {
    prices[0] = 0;
}
fn lowercase_first_ascii_character(s: &mut str) {
    if let Some(f) = s.get_mut(0..1) {
        f.make_ascii_lowercase();
    }
}

Solution 2

In addition to Shepmaster's answer, another reason to accept a &str (and similarly &[T] etc) is because of all of the other types besides String and &str that also satisfy Deref<Target = str>. One of the most notable examples is Cow<str>, which lets you be very flexible about whether you are dealing with owned or borrowed data.

If you have:

fn awesome_greeting(name: &String) {
    println!("Wow, you are awesome, {}!", name);
}

But you need to call it with a Cow<str>, you'll have to do this:

let c: Cow<str> = Cow::from("hello");
// Allocate an owned String from a str reference and then makes a reference to it anyway!
awesome_greeting(&c.to_string());

When you change the argument type to &str, you can use Cow seamlessly, without any unnecessary allocation, just like with String:

let c: Cow<str> = Cow::from("hello");
// Just pass the same reference along
awesome_greeting(&c);

let c: Cow<str> = Cow::from(String::from("hello"));
// Pass a reference to the owned string that you already have
awesome_greeting(&c);

Accepting &str makes calling your function more uniform and convenient, and the "easiest" way is now also the most efficient. These examples will also work with Cow<[T]> etc.

Share:
16,864

Related videos on Youtube

Shepmaster
Author by

Shepmaster

Cofounder of the world's first Rust consultancy — we are available to help you and your company with any and everything related to Rust! We also produce the Rust in Motion video series for Manning. I enjoy programming in a variety of languages, starting in C, touching on Java, moving to Ruby and most recently Rust. I also dabble in frontend frameworks like React and Redux. I love helping other people one-on-one and seeing them learn. To make sure that the software I write is useful and used, I am interested in interpersonal communication, as well as methodologies like Agile.

Updated on July 10, 2022

Comments

  • Shepmaster
    Shepmaster almost 2 years

    I wrote some Rust code that takes a &String as an argument:

    fn awesome_greeting(name: &String) {
        println!("Wow, you are awesome, {}!", name);
    }
    

    I've also written code that takes in a reference to a Vec or Box:

    fn total_price(prices: &Vec<i32>) -> i32 {
        prices.iter().sum()
    }
    
    fn is_even(value: &Box<i32>) -> bool {
        **value % 2 == 0
    }
    

    However, I received some feedback that doing it like this isn't a good idea. Why not?

  • Lukas Kalbertodt
    Lukas Kalbertodt over 7 years
    How about a tl;dr in the beginning? This answer is already somewhat long. Something like "&str is more general (as in: imposes less restrictions) without reduced capabilities"? Also: point 3 is often not that important I think. Usually Vecs and Strings will live on the stack and often even somewhere near the current stack frame. The stack is usually hot and the dereference will be served from a CPU cache.
  • Matthieu M.
    Matthieu M. over 7 years
    @Shepmaster: Regarding the allocation cost, it might be worth mentioning the particular issue of substrings/slices when talking about mandatory allocation. total_price(&prices[0..4]) does not require allocating a new vector for the slice.
  • C.Nivs
    C.Nivs over 5 years
    This is a great answer. I'm just getting started in Rust and was getting tied up figuring out when I should use a &str and why (coming from Python, so I usually don't explicitly deal with types). Cleared all of that up perfectly
  • Nawaz
    Nawaz over 5 years
    Awesome tips on parameters. Just need one doubt: "Accepting a &String, &Vec or &Box also requires an allocation before you can call the method. " ... Why is that so? Could you please point out the part in the docs where I can read this in detail? (I'm a begginner). Also, can we have similar tips on the return types?
  • Shepmaster
    Shepmaster over 5 years
    @Nawaz Box — "for heap allocation". String — "This buffer is always stored on the heap". Vec — "the memory it points to is on the heap"
  • cjohansson
    cjohansson about 5 years
    I'm missing information about why extra allocation is required. String is stored on the heap, when accepting &String as argument, why doesn't just Rust pass a pointer stored on the stack that points to the heap space, I don't understand why passing a &String would need additional allocation, passing a string slice should also require sending a pointer stored on the stack that points to the heap space?
  • Shepmaster
    Shepmaster about 5 years
    @cjohansson the storage on the heap that you mention is the extra allocation. The reference itself is passed as you describe. awesome_greeting(&String::from(“Anna”)) has an unneeded allocation from creating the string at all; awesome_greeting(“Anna”) does not. A string literal like that is not stored on the heap at all.
  • Xiao-Feng Li
    Xiao-Feng Li over 4 years
    It is probably easier to understand on the heap allocation part that, Vec and String can dynamically change their contents (size) at runtime, therefore are (almost) impossible to allocate on the stack; and Box is designed for heap allocation. So for function parameter types, it is better to use compatible types for them while not having the implication of mandatory heap allocation.
  • Arnavion
    Arnavion over 4 years
    For completeness, it should be noted that the only time it makes sense to accept &String or &Vec is when you need access to &self-taking methods that only exist on those, namely capacity. Furthermore, none of this applies to mutable borrows, ie &mut Vec or &mut String, which are legitimately needed when you want to grow or shrink the collection.