What you'll learn: Rust's ownership system — the single most important concept in the language. After this chapter you'll understand move semantics, borrowing rules, and the
Droptrait. If you grasp this chapter, the rest of Rust follows naturally. If you're struggling, re-read it — ownership clicks on the second pass for most C/C++ developers.
malloc() and freed with free(). No checks against dangling pointers, use-after-free, or double-freestd::move(ptr) compiles even after the move — use-after-move is UBFor C++ developers — Smart Pointer Mapping:
C++ Rust Safety Improvement std::unique_ptr<T>Box<T>No use-after-move possible std::shared_ptr<T>Rc<T>(single-thread)No reference cycles by default std::shared_ptr<T>(thread-safe)Arc<T>Explicit thread-safety std::weak_ptr<T>Weak<T>Must check validity Raw pointer *const T/*mut TOnly in unsafeblocksFor C developers:
Box<T>replacesmalloc/freepairs.Rc<T>replaces manual reference counting. Raw pointers exist but are confined tounsafeblocks.
ownershipborrow from the original owner. The rule is that the scope of the borrow can never exceed the owning scope. In other words, the lifetime of a borrow cannot exceed the owning lifetimefn main() {
let a = 42; // Owner
let b = &a; // First borrow
{
let aa = 42;
let c = &a; // Second borrow; a is still in scope
// Ok: c goes out of scope here
// aa goes out of scope here
}
// let d = &aa; // Will not compile unless aa is moved to outside scope
// b implicitly goes out of scope before a
// a goes out of scope last
}
&), or mutable (&mut)fn foo(x: &u32) {
println!("{x}");
}
fn bar(x: u32) {
println!("{x}");
}
fn main() {
let a = 42;
foo(&a); // By reference
bar(a); // By value (copy)
}
drop a reference when it goes out of scope.fn no_dangling() -> &u32 {
// lifetime of a begins here
let a = 42;
// Won't compile. lifetime of a ends here
&a
}
fn ok_reference(a: &u32) -> &u32 {
// Ok because the lifetime of a always exceeds ok_reference()
a
}
fn main() {
let a = 42; // lifetime of a begins here
let b = ok_reference(&a);
// lifetime of b ends here
// lifetime of a ends here
}
fn main() {
let s = String::from("Rust"); // Allocate a string from the heap
let s1 = s; // Transfer ownership to s1. s is invalid at this point
println!("{s1}");
// This will not compile
//println!("{s}");
// s1 goes out of scope here and the memory is deallocated
// s goes out of scope here, but nothing happens because it doesn't own anything
}
After let s1 = s, ownership transfers to s1. The heap data stays put — only the stack pointer moves. s is now invalid.
fn foo(s : String) {
println!("{s}");
// The heap memory pointed to by s will be deallocated here
}
fn bar(s : &String) {
println!("{s}");
// Nothing happens -- s is borrowed
}
fn main() {
let s = String::from("Rust string move example"); // Allocate a string from the heap
foo(s); // Transfers ownership; s is invalid now
// println!("{s}"); // will not compile
let t = String::from("Rust string borrow example");
bar(&t); // t continues to hold ownership
println!("{t}");
}
struct Point {
x: u32,
y: u32,
}
fn consume_point(p: Point) {
println!("{} {}", p.x, p.y);
}
fn borrow_point(p: &Point) {
println!("{} {}", p.x, p.y);
}
fn main() {
let p = Point {x: 10, y: 20};
// Try flipping the two lines
borrow_point(&p);
consume_point(p);
}
clone() method can be used to copy the original memory. The original reference continues to be valid (the downside is that we have 2x the allocation)fn main() {
let s = String::from("Rust"); // Allocate a string from the heap
let s1 = s.clone(); // Copy the string; creates a new allocation on the heap
println!("{s1}");
println!("{s}");
// s1 goes out of scope here and the memory is deallocated
// s goes out of scope here, and the memory is deallocated
}
clone() creates a separate heap allocation. Both s and s1 are valid — each owns its own copy.
Copy trait
copy semantics using the derive macro with to automatically implement the Copy trait// Try commenting this out to see the change in let p1 = p; below
#[derive(Copy, Clone, Debug)] // We'll discuss this more later
struct Point{x: u32, y:u32}
fn main() {
let p = Point {x: 42, y: 40};
let p1 = p; // This will perform a copy now instead of move
println!("p: {p:?}");
println!("p1: {p:?}");
let p2 = p1.clone(); // Semantically the same as copy
}
drop() method at the end of scope
drop is part of a generic trait called Drop. The compiler provides a blanket NOP implementation for all types, but types can override it. For example, the String type overrides it to release heap-allocated memoryfree() calls — resources are automatically released when they go out of scope (RAII).drop() directly (the compiler forbids it). Instead, use drop(obj) which moves the value into the function, runs its destructor, and prevents any further use — eliminating double-free bugsFor C++ developers:
Dropmaps directly to C++ destructors (~ClassName()):
C++ destructor Rust DropSyntax ~MyClass() { ... }impl Drop for MyType { fn drop(&mut self) { ... } }When called End of scope (RAII) End of scope (same) Called on move Source left in "valid but unspecified" state — destructor still runs on the moved-from object Source is gone — no destructor call on moved-from value Manual call obj.~MyClass()(dangerous, rarely used)drop(obj)(safe — takes ownership, callsdrop, prevents further use)Order Reverse declaration order Reverse declaration order (same) Rule of Five Must manage copy ctor, move ctor, copy assign, move assign, destructor Only Drop— compiler handles move semantics, andCloneis opt-inVirtual dtor needed? Yes, if deleting through base pointer No — no inheritance, so no slicing problem
struct Point {x: u32, y:u32}
// Equivalent to: ~Point() { printf("Goodbye point x:%u, y:%u\n", x, y); }
impl Drop for Point {
fn drop(&mut self) {
println!("Goodbye point x:{}, y:{}", self.x, self.y);
}
}
fn main() {
let p = Point{x: 42, y: 42};
{
let p1 = Point{x:43, y: 43};
println!("Exiting inner block");
// p1.drop() called here — like C++ end-of-scope destructor
}
println!("Exiting main");
// p.drop() called here
}
🟡 Intermediate — experiment freely; the compiler will guide you
Point with and without Copy in #[derive(Debug)] in the below make sure you understand the differences. The idea is to get a solid understanding of how move vs. copy works, so make sure to askDrop for Point that sets x and y to 0 in drop. This is a pattern that's useful for releasing locks and other resources for examplestruct Point{x: u32, y: u32}
fn main() {
// Create Point, assign it to a different variable, create a new scope,
// pass point to a function, etc.
}
#[derive(Debug)]
struct Point { x: u32, y: u32 }
impl Drop for Point {
fn drop(&mut self) {
println!("Dropping Point({}, {})", self.x, self.y);
self.x = 0;
self.y = 0;
// Note: setting to 0 in drop demonstrates the pattern,
// but you can't observe these values after drop completes
}
}
fn consume(p: Point) {
println!("Consuming: {:?}", p);
// p is dropped here
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // Move — p1 is no longer valid
// println!("{:?}", p1); // Won't compile: p1 was moved
{
let p3 = Point { x: 30, y: 40 };
println!("p3 in inner scope: {:?}", p3);
// p3 is dropped here (end of scope)
}
consume(p2); // p2 is moved into consume and dropped there
// println!("{:?}", p2); // Won't compile: p2 was moved
// Now try: add #[derive(Copy, Clone)] to Point (and remove the Drop impl)
// and observe how p1 remains valid after let p2 = p1;
}
// Output:
// p3 in inner scope: Point { x: 30, y: 40 }
// Dropping Point(30, 40)
// Consuming: Point { x: 10, y: 20 }
// Dropping Point(10, 20)