Rust

Why is it discouraged to accept a reference String Vec or Box as a function argument

19 September 2026 · 10 min read

Why is it discouraged to accept a reference String Vec or Box as a function argument

When writing Rust code, you might encounter situations where you’re tempted to accept references like &String, &Vec, or &Box as function arguments. While this seems straightforward, it’s generally discouraged. Understanding why it is discouraged to accept a reference &String, &Vec, or &Box as a function argument is crucial for writing idiomatic and efficient Rust code. These types represent different levels of indirection and ownership, and choosing the right type for your function parameters can significantly impact performance and flexibility. This post will delve into the reasons behind this recommendation, explore alternative approaches, and provide practical examples to illustrate best practices. We’ll also cover related concepts like string slices and vector slices, and explain how to use them effectively.

Understanding the Problem with &String, &Vec, and &Box

The core issue stems from Rust’s ownership and borrowing system. Types like String, Vec, and Box already own their underlying data. Taking a reference to them (&String, &Vec, &Box) introduces an unnecessary level of indirection. Instead, opting for more general types like &str and &[T] provides greater flexibility and avoids potential performance overhead. Consider the scenario where you have a function that needs to process a sequence of characters. If your function accepts &String, it can only operate on String instances. However, if it accepts &str, it can handle both String instances and string literals (&'static str), making your function more versatile.

Moreover, accepting &Box<t></t> as a function argument tightly couples your function to a specific allocation strategy. Box is a heap allocation, and requiring it limits the function’s usability. For instance, if you have a value on the stack, you would need to move it to the heap first just to pass it to the function. A more generic approach, such as accepting a reference to the underlying type (&T), allows the function to work with both stack-allocated and heap-allocated data. This aligns with Rust’s philosophy of zero-cost abstractions, where you pay only for what you use. By using the most general type possible, you avoid imposing unnecessary constraints on the caller.

In essence, favoring &str and &[T] over &String and &Vec promotes code reusability and reduces unnecessary overhead. As Steve Klabnik and Carol Nichols explain in “The Rust Programming Language,” using string slices and vector slices enhances the flexibility and efficiency of your Rust programs. See the Rust Book for more details.

Why &str and &[T] are Preferred

&str and &[T], known as string slices and slice types respectively, are dynamically sized views into contiguous sequences of data. They provide a flexible way to access data without taking ownership. A string slice (&str) is a reference to a sequence of UTF-8 encoded bytes, while a slice (&[T]) is a reference to a contiguous sequence of elements of type T. These types are more general than &String and &Vec because they can refer to any contiguous sequence, regardless of how it’s stored (e.g., string literals, parts of a String, arrays). This generality makes your functions more reusable and adaptable.

One of the key advantages of using &str and &[T] is that they can be created from various sources without requiring data copying. For example, you can easily create a &str from a String using the dereference operator (&my_string). Similarly, you can create a &[T] from a Vec<t></t> using the same approach (&my_vec). This avoids unnecessary memory allocation and copying, leading to more efficient code. Furthermore, these slice types are fundamental to Rust’s borrowing system, enabling safe and efficient access to data without ownership transfer.

Consider this featured snippet-optimized paragraph: Using &str and &[T] as function arguments makes your code more generic and efficient because they can work with both owned data types (like String and Vec) and borrowed data (like string literals and array slices). This avoids unnecessary data copying and allocation, improving performance and reducing memory usage. By accepting slices, your functions become more versatile and can handle a wider range of inputs without requiring conversions or ownership transfers.

Practical Examples and Use Cases

Let’s illustrate with a practical example. Suppose you want to write a function that counts the number of vowels in a string. A naive implementation might accept &String as an argument. However, a better approach would be to accept &str. Here’s how it looks:

fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(c)).count() } fn main() { let my_string = String::from("Hello, World!"); let vowel_count = count_vowels(&my_string); println!("Vowel count: {}", vowel_count); let string_literal = "Rust is awesome!"; let vowel_count_literal = count_vowels(string_literal); println!("Vowel count (literal): {}", vowel_count_literal); } 

As you can see, the count_vowels function accepts &str, allowing it to work with both a String instance and a string literal. If the function had accepted &String, it would only be able to process String instances, making it less flexible. This same principle applies to vectors. If you have a function that needs to process a sequence of numbers, accept &[i32] instead of &Vec<i32></i32>.

Here is an example involving vectors:

fn sum_elements(numbers: &[i32]) -> i32 { numbers.iter().sum() } fn main() { let my_vector = vec![1, 2, 3, 4, 5]; let sum = sum_elements(&my_vector); println!("Sum: {}", sum); let array = [6, 7, 8, 9, 10]; let sum_array = sum_elements(&array); println!("Sum (array): {}", sum_array); } 

In this case, the sum_elements function accepts &[i32], allowing it to work with both a Vec<i32></i32> and an array. This illustrates the power and flexibility of using slices in Rust.

Alternative Approaches and Best Practices

When designing your function signatures, consider the following guidelines to ensure optimal flexibility and performance:

  • Prefer &str over &String: This allows your function to work with both owned strings and string literals.
  • Prefer &[T] over &Vec<t></t>: This allows your function to work with both owned vectors and array slices.
  • Avoid &Box<t></t> unless necessary: Consider accepting &T instead to avoid restricting the caller to heap-allocated data.

In some cases, you might need to own the data passed to your function. For example, if the function needs to modify the data or store it for later use. In such cases, accepting String or Vec<t></t> is appropriate. However, always consider whether a reference (&str or &[T]) is sufficient before opting for ownership transfer. Here’s an example:

  1. Analyze the Function’s Purpose: Determine if the function needs to modify or own the data.
  2. Choose the Most General Type: If possible, use &str or &[T] to maximize flexibility.
  3. Consider Ownership: If the function needs to own the data, accept String or Vec<t></t>.
  4. Avoid Unnecessary Indirection: Avoid &String, &Vec<t></t>, and &Box<t></t> unless specifically required.

Another best practice is to use the Cow (Clone on Write) type when you need to potentially modify the input data but want to avoid unnecessary cloning. Cow allows you to work with either a borrowed slice or an owned value, cloning the data only when modification is needed. This can be particularly useful in scenarios where the input data is often read-only but occasionally requires modification.

Infographic here showing the different levels of indirection and memory allocation for String, &String, &str, Vec, &Vec, and &[T].
FAQ ---
Why is `&String` discouraged?
`&String` introduces an unnecessary level of indirection. `&str` is more flexible and can handle both `String` instances and string literals.
What is the difference between `&str` and `String`?
`String` owns its data, while `&str` is a borrowed view into a string. `&str` is more general and can refer to string literals or parts of a `String`.
When should I use `String` instead of `&str`?
Use `String` when you need to own and potentially modify the string data.
What are the benefits of using slices (`&[T]`)?
Slices provide a flexible way to access contiguous sequences of data without taking ownership, avoiding unnecessary copying and allocation. They are more generic than `&Vec`.
How does this relate to zero-cost abstractions?
Using `&str` and `&[T]` allows you to avoid unnecessary memory allocation and copying, aligning with Rust's goal of zero-cost abstractions. You only pay for what you use.
By applying these principles, you can write more efficient and maintainable Rust code. Consider leveraging online resources like [Stack Overflow](https://stackoverflow.com/) and [Rust's user forum](https://users.rust-lang.org/) for further insights and community support.

Hopefully, this exploration sheds light on why it is discouraged to accept a reference &String, &Vec, or &Box as a function argument. By embracing &str and &[T], you unlock greater flexibility and efficiency in your Rust code. Remember to always consider ownership and borrowing principles when designing your function signatures. It’s a matter of writing code that is both performant and easy to understand.

Now, armed with this knowledge, take a look at your existing Rust projects. Are there places where you’re using &String or &Vec unnecessarily? Refactoring these areas can lead to significant improvements in performance and code clarity. For further learning, explore Rust’s documentation on ownership and borrowing and consider reading about the Cow type for advanced scenarios. And don’t hesitate to reach out if you have more questions.

Question & Answer :
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?

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(); } }