#126 May 8, 2026

126. Vec::split_off — Cut a Vec in Two and Keep Both Halves

You want the tail of a Vec as its own owned collection — the head stays put, the tail walks away. Cloning a slice works for Clone types, but breaks the moment your elements aren’t cloneable. Vec::split_off doesn’t care.

The clone-and-truncate dance

The textbook split: copy the tail with to_vec(), then truncate the original.

1
2
3
4
5
6
7
let mut all = vec![1, 2, 3, 4, 5, 6];

let tail: Vec<i32> = all[3..].to_vec();
all.truncate(3);

assert_eq!(all,  vec![1, 2, 3]);
assert_eq!(tail, vec![4, 5, 6]);

It works, but it clones every element of the tail. Fine for i32, wasteful for String, and a hard error for any type that isn’t Clone.

split_off moves, doesn’t clone

Vec::split_off(at) consumes the elements at at.. out of the original Vec and returns them as a new Vec. The elements are moved, not copied — so it works for any T, Clone or not:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let mut tasks: Vec<Box<dyn FnOnce()>> = vec![
    Box::new(|| println!("a")),
    Box::new(|| println!("b")),
    Box::new(|| println!("c")),
    Box::new(|| println!("d")),
];

// Box<dyn FnOnce()> isn't Clone — `tasks[2..].to_vec()` won't compile.
let later = tasks.split_off(2);

assert_eq!(tasks.len(), 2);
assert_eq!(later.len(), 2);

The original keeps [0..at), the returned Vec gets [at..len), and not a single element is duplicated. at == 0 gives you the whole thing in the new Vec (the original ends up empty); at == len gives you an empty new Vec. Anything past the length panics.

A real shape: page-by-page draining

split_off shines when you want to peel a batch off the front of a queue and hand it to a worker, keeping the rest for next time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn next_batch<T>(queue: &mut Vec<T>, size: usize) -> Vec<T> {
    let take = size.min(queue.len());
    let rest = queue.split_off(take);
    // queue now holds the first `take` items — that's our batch.
    // `rest` holds everything else — put it back as the new queue.
    let batch = std::mem::replace(queue, rest);
    batch
}

let mut q = vec!["a", "b", "c", "d", "e"];
let first  = next_batch(&mut q, 2);
let second = next_batch(&mut q, 2);

assert_eq!(first,  vec!["a", "b"]);
assert_eq!(second, vec!["c", "d"]);
assert_eq!(q,      vec!["e"]);

No clone, no temporary Vec, no fighting the borrow checker over slice ranges. Two memcpys and you’re done.

When to reach for it

Use split_off whenever you need both halves of a Vec as owned collections — batching, chunked processing, splitting state between threads. If you only want to iterate the tail and throw it away, drain(at..) is better; if you want to keep it, split_off is the move.

← Previous 125. RwLockWriteGuard::downgrade — Hand a Write Lock Off as a Read, Atomically