If you have been writing Rust for a while, you probably survived the ownership chapter in the official book. You know that values have a single owner, that assignment moves values, and that the borrow checker will reject your code if you try to use something after it has been moved. That much is the foundation.
But there is a big gap between knowing the rules and understanding the model well enough to write production-grade Rust confidently. The developers who fight the borrow checker every day are usually not violating the rules – they are missing the mental model behind them.
This is Part 1 of the Advanced Rust: Ownership, Borrowing and Lifetimes series. We are going to dig into ownership at a level that actually changes how you write code – not just how you survive the compiler.
What Ownership Actually Means
Ownership in Rust is not just a memory management strategy. It is a way of expressing resource lifetimes through the type system. When a value has an owner, the compiler knows exactly when that value will be cleaned up – at the end of the owner’s scope. No runtime garbage collector, no reference counting by default, no destructor calls you have to remember to make.
The rule is simple: every value has exactly one owner at any point in time. When the owner goes out of scope, the value is dropped. This is deterministic, zero-cost, and happens at compile time.
The key insight that most beginner resources skip is that ownership is about resource management in the general sense, not just heap memory. A file handle owns a file descriptor. A mutex guard owns a lock. A network connection owns a socket. When any of these go out of scope, the resource is released automatically. This is Rust’s version of RAII (Resource Acquisition Is Initialization), borrowed from C++ but enforced at the language level.
Move Semantics in Real Code
In beginner examples, move semantics look like this:
let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
// println!("{}", s1); // compile error: value borrowed after moveSimple enough. But in production code, moves are happening constantly in ways that are less obvious. Consider this:
fn process(config: Config) {
// config is moved into this function
// the caller no longer owns it
}
fn main() {
let config = Config::new("production");
process(config);
// process(config); // compile error: use of moved value
}Every time you pass a non-Copy value to a function, you are moving it. The function takes ownership. The caller loses it. This is not a bug – it is the language being explicit about resource lifetimes. When process returns, it drops config.
This becomes significant at scale. If you have a value that multiple functions need, you have four options:
- Pass a reference (borrow it)
- Clone it before passing
- Return it from the function after use
- Redesign so only one function needs it
Experienced Rust developers default to option one – borrowing – almost every time. Cloning has a cost and is a design smell if overused. Returning values works well for builder patterns. Redesigning is sometimes the right answer when ownership fights signal a structural problem.
The Ownership Transfer Flow
Here is how ownership moves through a typical Rust program:
flowchart TD
A[Value Created\non Stack or Heap] --> B[Owned by Variable]
B --> C{Assigned or Passed?}
C -->|Move - non-Copy type| D[New Owner]
C -->|Copy - Copy type| E[Both Valid\nBitwise Copy]
C -->|Borrow| F[Temporary Reference\nOwnership Stays]
D --> G{Function Returns?}
G -->|Returned| H[Caller Takes Ownership]
G -->|Not Returned| I[Dropped at End of Scope]
F --> J[Reference Expires\nOwner Regains Full Control]
E --> K[Original + Copy Both Dropped\nat Their Own Scopes]
Copy Types: What They Are and Why They Exist
Not every type in Rust uses move semantics. Types that implement the Copy trait are duplicated automatically on assignment rather than moved. The original stays valid.
let x: i32 = 42;
let y = x; // x is NOT moved - it is copied
println!("x={}, y={}", x, y); // both work fineThe rule for what can be Copy is strict: a type can implement Copy only if a bitwise copy of its bytes creates a fully independent, valid value. Integers, booleans, floats, and tuples or arrays of Copy types all qualify. String, Vec, and any type that owns heap memory do not – because a bitwise copy would create two owners pointing at the same heap allocation, and when both try to drop it, you get a double-free error.
There is also a hard rule: a type that implements Drop cannot implement Copy. If your type needs custom cleanup logic, it cannot be trivially copied.
Here is a quick reference for common types:
| Type | Copy? | Reason |
|---|---|---|
| i8, i16, i32, i64, i128, isize | Yes | Fixed size, stack only |
| u8, u16, u32, u64, u128, usize | Yes | Fixed size, stack only |
| f32, f64 | Yes | Fixed size, stack only |
| bool, char | Yes | Fixed size, stack only |
| &T (shared reference) | Yes | Pointer is cheap to copy |
| String | No | Owns heap memory |
| Vec<T> | No | Owns heap memory |
| Box<T> | No | Single ownership semantics |
| (T, U) where T and U are Copy | Yes | All parts are Copy |
| [T; N] where T is Copy | Yes | All elements are Copy |
Implementing Copy on Your Own Types
You can derive Copy on your own structs and enums as long as all fields are also Copy:
#[derive(Debug, Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
fn translate(p: Point, dx: f64, dy: f64) -> Point {
Point { x: p.x + dx, y: p.y + dy }
}
fn main() {
let origin = Point { x: 0.0, y: 0.0 };
let moved = translate(origin, 3.0, 4.0);
// origin is still valid because Point is Copy
println!("origin: {:?}", origin);
println!("moved: {:?}", moved);
}Notice that Copy requires Clone to also be implemented. This is because Copy is a subtrait of Clone – anything that can be bitwise copied can also be explicitly cloned (cloning just does the same thing). You almost always derive both together.
A practical guideline: derive Copy when your type is small, represents a value (coordinates, colors, identifiers), and has no heap allocations. Avoid it on types that are meant to represent ownership of a resource.
Clone: Explicit Deep Copying
Clone is the explicit counterpart to Copy. Calling .clone() produces a new, fully independent copy of the value. For types like String or Vec, this means allocating new heap memory and copying all the data into it.
let original = vec![1, 2, 3, 4, 5];
let cloned = original.clone(); // new heap allocation, full copy
// both are independently owned
println!("{:?}", original);
println!("{:?}", cloned);The .clone() call is intentionally verbose. It is a visual signal that potentially expensive work is happening. When you see a .clone() in a code review, it is worth asking: is this necessary, or can we restructure to use a borrow instead?
You can implement Clone manually when you need custom duplication logic:
#[derive(Debug)]
struct CacheEntry {
key: String,
value: Vec,
hit_count: u64,
}
impl Clone for CacheEntry {
fn clone(&self) -> Self {
CacheEntry {
key: self.key.clone(),
value: self.value.clone(),
hit_count: 0, // reset hit count on clone - intentional design choice
}
}
}
fn main() {
let entry = CacheEntry {
key: String::from("user:42"),
value: vec![1, 2, 3],
hit_count: 150,
};
let cloned = entry.clone();
println!("Original hit count: {}", entry.hit_count); // 150
println!("Cloned hit count: {}", cloned.hit_count); // 0
} Ownership Through Functions: The Patterns That Actually Matter
Here is where things get practical. These are the patterns you will use constantly in real Rust code.
Pattern 1: Take Ownership When You Need to Store the Value
struct Server {
config: Config, // Server owns the Config
}
impl Server {
// Takes ownership of config - caller gives it up
fn new(config: Config) -> Self {
Server { config }
}
}When a struct needs to store a value for its lifetime, take ownership in the constructor. The caller no longer needs it, so taking it is the right design.
Pattern 2: Borrow When You Only Need to Read
fn log_config(config: &Config) {
// borrows config, does not take ownership
println!("Config: {:?}", config);
}
fn main() {
let config = Config::new("production");
log_config(&config);
// config is still valid here
start_server(config); // now we move it
}Pattern 3: Return Ownership to Thread It Through
fn with_timeout(mut config: Config, timeout_ms: u64) -> Config {
config.timeout = timeout_ms;
config // return ownership back to caller
}
fn main() {
let config = Config::new("production");
let config = with_timeout(config, 5000); // ownership threads through
start_server(config);
}This pattern – take, modify, return – is the basis of builder patterns in Rust. It is verbose compared to mutable references but makes the transformation explicit.
The Drop Order and Why It Matters
When multiple values are in scope, Rust drops them in reverse order of declaration. This is deterministic and important for resources that depend on each other:
fn main() {
let connection = DatabaseConnection::new(); // created first
let transaction = connection.begin(); // depends on connection
// transaction is dropped first (last declared = first dropped)
// connection is dropped second
// this guarantees transaction is closed before connection is released
}Fields within a struct are dropped in declaration order. This matters when you have fields that depend on each other – put the dependent field first so it is dropped first.
Ownership and the Drop Trait
The Drop trait is how you hook into the cleanup that happens when ownership ends. It is Rust’s destructor:
#[derive(Debug)]
struct TempFile {
path: String,
}
impl TempFile {
fn new(path: &str) -> Self {
// create the file
std::fs::File::create(path).unwrap();
TempFile { path: path.to_string() }
}
}
impl Drop for TempFile {
fn drop(&mut self) {
// guaranteed cleanup when TempFile goes out of scope
if std::fs::remove_file(&self.path).is_err() {
eprintln!("Warning: failed to remove temp file {}", self.path);
}
}
}
fn main() {
{
let tmp = TempFile::new("/tmp/workfile.tmp");
// do work with tmp
println!("Working with {:?}", tmp);
} // tmp dropped here, file deleted automatically
println!("Temp file has been cleaned up");
}You can also force an early drop with std::mem::drop(value). This moves the value into drop, which immediately disposes of it. This is useful for releasing locks or file handles before the end of a scope.
Common Mistakes and How to Think About Them
Mistake 1: Cloning to Avoid Thinking About Ownership
Early-stage Rust code often sprinkles .clone() everywhere to make the compiler happy. It works, but it is masking a design issue. Every .clone() is a sign that you have not decided who owns what. Fix the design, not the symptoms.
Mistake 2: Using Arc When You Do Not Need Shared Ownership
Arc<T> is for shared ownership across threads. If you are not crossing thread boundaries and do not actually have multiple owners, you probably need a reference, not an Arc. Arc has reference-counting overhead and implies a design where no single owner is responsible – that is sometimes right, but often it is overkill.
Mistake 3: Partial Moves in Structs
struct User {
name: String,
email: String,
}
fn main() {
let user = User {
name: String::from("Alice"),
email: String::from("alice@example.com"),
};
let name = user.name; // partial move - user.name is moved out
// println!("{}", user.name); // error: value partially moved
println!("{}", user.email); // this is still fine
// println!("{:?}", user); // error: user is partially moved
}Partial moves leave the struct in a partially-moved state. You can still use the non-moved fields individually, but you cannot use the struct as a whole. This is often surprising. The fix is usually to either take a reference, use .clone() on the field you want, or destructure with let User { name, email } = user to move both fields explicitly.
What Comes Next
Ownership gives Rust its memory safety guarantees. But ownership alone does not let you write flexible, reusable code – for that you need borrowing. In Part 2 of this series, we will go deep on the borrowing rules, look at how the borrow checker reasons about your code, and explore the patterns that let you work with the borrow checker rather than against it.
The concepts in this post – moves, Copy, Clone, Drop, partial moves – are the vocabulary you need to make sense of what the borrow checker is telling you. Get comfortable with them, and Part 2 will be significantly more productive.
References
- The Rust Programming Language – “What is Ownership?” (https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html)
- JetBrains RustRover Blog – “Everything You Wanted to Ask About Rust” (https://blog.jetbrains.com/rust/2025/12/23/everything-you-wanted-to-ask-about-rust-answered-by-herbert-wolverson/)
- JetBrains RustRover Blog – “The State of Rust Ecosystem 2025” (https://blog.jetbrains.com/rust/2026/02/11/state-of-rust-2025/)
- Dev.to – “Mastering Copy and Clone traits in Rust” (https://dev.to/amritsingh183/mastering-copy-and-clone-traits-in-rust-455e)
- Tangram Vision – “C++ and Rust: Interior Mutability, Moving and Ownership” (https://www.tangramvision.com/blog/c-rust-interior-mutability-moving-and-ownership)
- CodeForGeek – “Copy Types vs. Move Semantics in Rust” (https://codeforgeek.com/copy-types-vs-move-semantics-in-rust/)
