8.4 Why Strings and Vecs Are Not Copy
Right, so you’ve met Clone and Copy, our two best friends for lazily duplicating data. You might be looking at String and Vec and thinking, “These seem fundamental. Why on earth aren’t they Copy? Wouldn’t that be convenient?” Oh, my sweet summer child. If they were Copy, it would be a one-way ticket to performance hell and a special kind of memory safety nightmare. Let me show you why.
The entire raison d’être for Copy is that it represents a trivial bit-for-bit duplication. Think integers. When you do let y = x; where x is an integer, you just get two identical numbers sitting in two different registers or stack slots. No big deal. The cost is microscopic. Now, consider what a String or a Vec actually is under the hood.
The Gory Details of a Growable Buffer
A String isn’t the text itself; it’s a manager for a heap-allocated buffer. It’s a struct that contains a pointer to the bytes on the heap, a capacity (how much total space it has), and a length (how much space is actually used). A Vec is the same idea, but for any type.
// This is a massive simplification, but the spirit is correct.
pub struct String {
ptr: *mut u8,
cap: usize,
len: usize,
}
If String were Copy, the act of assigning it would duplicate only this handful of bytes. The pointer, the cap, the len. You’d end up with two separate String values, both pointing to the exact same chunk of memory on the heap.
// IMAGINE THIS HORROR (thankfully, it doesn't compile)
let s1 = String::from("Hello");
let s2 = s1; // If String were Copy, both s1 and s2 are now valid.
// You've just created a classic double-free scenario.
} // When both go out of scope, they both try to free the SAME memory. 💥
This is an undisputed catastrophe. Both s1 and s2 would try to free the same memory when they go out of scope. Undefined Behavior. Segfaults. The wrath of a thousand angry systems programmers. The Drop trait, which defines how a value is cleaned up, is what makes this impossible. A type cannot be Copy if it implements Drop—the compiler forbids it. This is a brilliantly simple rule that saves us from ourselves.
So We Use Clone. What’s the Big Deal?
Instead, String and Vec are only Clone. The .clone() method is explicit for a very good reason: it’s potentially expensive. It’s not just copying three integers; it’s making a whole new allocation on the heap and copying the entire contents of the original buffer into it.
let v1 = vec![1, 2, 3, 4, 5];
let v2 = v1.clone(); // This triggers a full, heap allocation and a copy of all 5 elements.
println!("v1: {:?}, v2: {:?}", v1, v2); // This is fine because v1 is still valid.
This explicitness is a gift. It’s a giant, flashing sign in your code that says, “HEY, PAY ATTENTION, THIS LINE MIGHT BE COSTLY.” If these types were Copy, this expensive operation would happen silently, implicitly, every time you passed a Vec to a function. You’d be allocating and copying left and right without any visual cue that you’re tanking your program’s performance.
The Move is the Magic
Because they aren’t Copy, the assignment let v2 = v1; moves the value. Ownership of the heap-allocated buffer is transferred. v1 becomes invalid. This is the cornerstone of Rust’s memory management without a garbage collector.
let s1 = String::from("world");
let s2 = s1; // Ownership moves from s1 to s2.
// println!("{}", s1); // Error! value borrowed after move
This move semantics is zero-cost. It’s the same trivial copy of those three backend fields (ptr, cap, len) that would have happened if it were Copy, but with the crucial added step of invalidating the original. We get the cheap, fast operation of a copy, but with the safety of knowing there’s only one owner responsible for that heap memory. It’s the best of both worlds: efficiency and safety. Making them Copy would throw away the safety, and making the move expensive would throw away the efficiency. The current design is, frankly, a masterpiece.
The takeaway? Be grateful String and Vec aren’t Copy. The inconvenience of typing .clone() is a tiny price to pay for the performance clarity and memory safety you get in return. It forces you to think about ownership from the very beginning, which is the whole point of using Rust in the first place.