Re-pointing the caller's slice: &mut &mut [T]¶
References › Re-pointing a slice
Level: 201 · working knowledge
One line: t: &mut &mut [i32] is a &mut to the caller's slice reference, so the function can move where the caller's view starts and ends — which a plain t: &mut [i32] can only do to its own copy.
fn skip_first(t: &mut &mut [i32]) {
let whole = std::mem::take(t); // the caller's &mut [i32], by value; `&mut []` left behind
*t = &mut whole[1..]; // hand back a shorter one
}
fn main() {
let mut data = [1, 2, 3];
let mut view: &mut [i32] = &mut data;
skip_first(&mut view);
println!("{view:?}"); // [2, 3]
}
data still holds three elements. What changed is view: a slice reference is two words, an address and a length (the second word), and skip_first rewrote both in the caller's variable.
Three parameters, three reaches¶
| parameter | writes elements | moves the caller's view | grows the collection |
|---|---|---|---|
t: &mut [i32] |
yes | no — only its own copy | no |
t: &mut &mut [i32] |
yes | yes | no |
v: &mut Vec<i32> |
yes | — | yes, v.push(4) |
A slice has no push: t.push(4) on a &mut [i32] is E0599, no method named push found for mutable reference &mut [i32]. Growing needs the Vec; re-slicing a slice only ever narrows it.
A plain &mut [i32] re-points its own copy¶
fn rebind_local(mut t: &mut [i32]) {
t[0] = 99; // writes the caller's data
t = &mut t[1..]; // re-points the local `t`
t[0] = 42; // writes data[1]
}
fn main() {
let mut data = [1, 2, 3];
let view: &mut [i32] = &mut data;
rebind_local(view);
println!("{view:?}"); // [99, 42, 3]
}
Both writes reach the caller. The re-point does not: t is a parameter, a local that received the caller's (address, length) pair by reborrow, and assigning to it changes the local. To change the caller's pair, the function needs a &mut to it — one more layer.
Why not *t = &mut t[1..]¶
error: lifetime may not live long enough
--> skip_first_naive.rs:2:5
|
1 | fn skip_first(t: &mut &mut [i32]) {
| - - let's call the lifetime of this reference `'2`
| |
| let's call the lifetime of this reference `'1`
2 | *t = &mut t[1..];
| ^^^^^^^^^^^^^^^^ assignment requires that `'1` must outlive `'2`
|
help: consider introducing a named lifetime parameter
|
1 | fn skip_first<'a>(t: &'a mut &'a mut [i32]) {
| ++++ ++ ++
'1 is the outer borrow, the &mut view at the call, which only has to cover the call; '2 is the caller's slice reference, which lives on after it. t[1..] reaches the elements through t, so &mut t[1..] is a reborrow and lasts no longer than '1 — and *t must hold something that lasts '2. A &mut reached through another &mut cannot outlive the outer one; what &'a T claims has the reason.
std::mem::take ↗ is the way round. pub fn take<T>(dest: &mut T) -> T where T: Default moves the caller's &'2 mut [i32] out of *t and leaves the default in its place — impl<T> Default for &mut [T] is the empty slice. whole is then the function's own &'2 mut [i32], not a reborrow through t, so &mut whole[1..] lasts '2 as well. This is the mem::take move: the hole is filled in the same call that empties it.
rustc's help: compiles, and breaks the caller¶
Accept the suggestion — fn skip_first<'a>(t: &'a mut &'a mut [i32]) — and the function compiles. The call site does not:
error[E0502]: cannot borrow `view` as immutable because it is also borrowed as mutable
--> skip_first_one_lifetime.rs:9:16
|
8 | skip_first(&mut view);
| --------- mutable borrow occurs here
9 | println!("{view:?}");
| ^^^^
| |
| immutable borrow occurs here
| mutable borrow later used here
One 'a for both layers says the borrow of view lasts as long as the slice reference inside it, so &mut view stays live for the rest of view's life and nothing can read view again. That is borrowing something forever, with a slice reference as the thing borrowed. The two lifetimes need to stay different, and mem::take is what lets them.
Handing out an element: name the inner lifetime¶
fn pop_front<'a>(t: &mut &'a mut [i32]) -> Option<&'a mut i32> {
let (first, rest) = std::mem::take(t).split_first_mut()?;
*t = rest;
Some(first)
}
'a is the caller's slice lifetime, so each popped element outlives the call and two can be held at once. The example pops a and b from [1, 2, 3], adds to both, and ends with view = [3] and data = [11, 22, 3].
Leave the lifetimes out and rustc will not guess:
error[E0106]: missing lifetime specifier
--> pop_front_elided.rs:1:44
|
1 | fn pop_front(t: &mut &mut [i32]) -> Option<&mut i32> {
| --------------- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say which one of `t`'s 2 lifetimes it is borrowed from
Its help: again offers &'a mut &'a mut [i32], with the caller-side cost above. Naming the outer lifetime instead — fn pop_front<'a, 'b>(t: &'b mut &'a mut [i32]) -> Option<&'b mut i32> — compiles, and ties each element to a borrow of view:
error[E0499]: cannot borrow `view` as mutable more than once at a time
--> pop_front_outer.rs:11:23
|
10 | let a = pop_front(&mut view).unwrap();
| --------- first mutable borrow occurs here
11 | let b = pop_front(&mut view).unwrap();
| ^^^^^^^^^ second mutable borrow occurs here
12 | *a += 10;
| -------- first borrow later used here
The shared version needs no take¶
With &mut &[i32] the naive line compiles as written:
&[i32] is Copy, so reading *t copies the caller's reference out with its full lifetime, and the re-slice lasts as long as that copy. The same holds for &str, which makes &mut &str the usual shape of a parser cursor:
fn next_word<'a>(input: &mut &'a str) -> Option<&'a str> {
let s = input.trim_start();
if s.is_empty() {
*input = s;
return None;
}
let end = s.find(' ').unwrap_or(s.len());
let (word, rest) = s.split_at(end);
*input = rest; // the caller's cursor now starts after the word
Some(word) // borrowed from the original text, not from `input`
}
On "GET /index.html HTTP/1.1", two calls return Some("GET") and Some("/index.html") and leave the cursor at " HTTP/1.1"; each word has lifetime 'a, so it outlives the cursor moving on.
std does this: Read for &[u8], Write for &mut [u8]¶
impl Read for &[u8] makes Self = &[u8], so fn read(&mut self, buf: &mut [u8]) receives a &mut &[u8] — this page's shared shape — and advances it past what it copied:
use std::io::Read;
fn main() {
let mut src: &[u8] = b"hello";
let mut buf = [0u8; 2];
let n = src.read(&mut buf).unwrap();
println!("{n} {:?}", std::str::from_utf8(src).unwrap()); // 2 "llo"
}
impl Write for &mut [u8] is the &mut &mut [u8] shape exactly. Its body in 1.98.0's library/std/src/io/impls.rs ↗ is let (a, b) = mem::take(self).split_at_mut(amt);, then *self = b; — the skip_first above. Write b"hi" into a five-byte buffer through it and the slice left over is three bytes long; the buffer reads "hi...". This is why a &mut [u8] can stand in for a file in tests of Read and Write: each call consumes the front of the slice.
Checkpoint¶
Predict before you open the answer. skip_first runs twice on a view of [1, 2, 3]. What do view and data print?
The answer
Verified output of repointing_a_slice.rs — regenerated by tools/run_examples.py, never hand-typed.
1. &mut &mut [i32]: the function moves the caller's view
skip_first(&mut view) -> view = [2, 3]
2. &mut [i32]: elements yes, the caller's view no
rebind_local(view) -> view = [99, 42, 3]
`t = &mut t[1..]` re-pointed the function's copy; view is still 3 long
3. &mut Vec<i32>: the one that can grow
grow(&mut v) -> v = [1, 2, 3, 4]
4. Handing out elements that outlive the call
a, b popped, both still live -> view = [3]
after *a += 10 and *b += 20 -> data = [11, 22, 3]
5. The shared version: &mut &[i32], and &mut &str as a parser cursor
skip_first_shared(&mut view) -> view = [2, 3]
two next_word calls -> Some("GET"), Some("/index.html")
cursor -> " HTTP/1.1"
two more -> Some("HTTP/1.1"), None, cursor = ""
6. std: `impl Read for &[u8]` receives &mut &[u8]
src.read(&mut buf) -> read 2: "he"
src afterwards -> "llo", len 3
7. std: `impl Write for &mut [u8]` receives &mut &mut [u8]
dst.write(b"hi") -> wrote 2, dst len now 3
out -> "hi..."
Checkpoint. skip_first twice on [1, 2, 3]: what do view and data print?
view = [3]
data = [1, 2, 3] (the view moved; nothing was removed from data)
Practice¶
Split off the header. Write fn take_header<'a>(input: &mut &'a [u8], n: usize) -> Option<&'a [u8]>: it returns the first n bytes and moves the caller's slice past them, or, when there are fewer than n, returns None and leaves the caller's slice as it was. Then write its twin over &mut &'a mut [u8], returning Option<&'a mut [u8]>, take two headers off one buffer, and write into both while both are held. Print the twin's slice length after a None: one natural way to write it compiles and leaves the caller with an empty slice.
Solution
repointing_a_slice_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: split off the header.
//!
//! Both functions return the first `n` bytes and advance the caller's slice past
//! them, or return `None` and leave the caller's slice as it was. The trap is in
//! the second: `mem::take` empties the caller's slice first, so a `?` after it
//! returns `None` and leaves the caller holding the empty slice.
//! `take_header_mut_early` below is that version, run.
//!
//! rustc --edition 2024 repointing_a_slice_kata.rs -o /tmp/repointing_a_slice_kata && /tmp/repointing_a_slice_kata
use std::mem;
/// Shared: `&'a [u8]` is `Copy`, so `split_at_checked` works on a copy of the
/// caller's slice and both halves keep `'a`. No `take` needed.
fn take_header<'a>(input: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
let (header, rest) = input.split_at_checked(n)?; // `None` returns before any write
*input = rest;
Some(header)
}
/// Exclusive: check the length while the caller's slice is still in place, then
/// take it. Past the check, `split_at_mut` cannot panic.
fn take_header_mut<'a>(input: &mut &'a mut [u8], n: usize) -> Option<&'a mut [u8]> {
if input.len() < n {
return None;
}
let (header, rest) = mem::take(input).split_at_mut(n);
*input = rest;
Some(header)
}
/// Compiles, and breaks the contract: on `None` the `&mut []` that `take` left
/// behind is what the caller keeps.
fn take_header_mut_early<'a>(input: &mut &'a mut [u8], n: usize) -> Option<&'a mut [u8]> {
let (header, rest) = mem::take(input).split_at_mut_checked(n)?;
*input = rest;
Some(header)
}
fn main() {
println!("1. take_header on &mut &[u8]");
let packet: [u8; 6] = [0xCA, 0xFE, 0x00, 0x02, 0x68, 0x69];
let mut rest: &[u8] = &packet;
let magic = take_header(&mut rest, 2);
let len = take_header(&mut rest, 2);
println!(" take 2, take 2 -> {magic:02X?}, {len:02X?}");
println!(" rest -> {rest:02X?}");
let too_long = take_header(&mut rest, 9);
println!(" take 9 -> {too_long:?}, rest still {rest:02X?}");
println!();
println!("2. take_header_mut on &mut &mut [u8]");
let mut buf = [0u8; 6];
let mut view: &mut [u8] = &mut buf;
let magic = take_header_mut(&mut view, 2).unwrap();
let len = take_header_mut(&mut view, 2).unwrap(); // `magic` is still live: both have 'a
magic.copy_from_slice(&[0xCA, 0xFE]);
len.copy_from_slice(&2u16.to_be_bytes());
println!(" take 2, take 2, write both -> view left {} long", view.len());
let too_long = take_header_mut(&mut view, 9);
println!(" take 9 -> {too_long:?}, view still {} long", view.len());
view.copy_from_slice(b"hi");
println!(" buf -> {buf:02X?}");
println!();
println!("3. The early `?` after mem::take");
let mut buf = [0u8; 6];
let mut view: &mut [u8] = &mut buf;
let too_long = take_header_mut_early(&mut view, 9);
println!(" take 9 -> {too_long:?}, view now {} long", view.len());
}
Verified output of repointing_a_slice_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. take_header on &mut &[u8]
take 2, take 2 -> Some([CA, FE]), Some([00, 02])
rest -> [68, 69]
take 9 -> None, rest still [68, 69]
2. take_header_mut on &mut &mut [u8]
take 2, take 2, write both -> view left 2 long
take 9 -> None, view still 2 long
buf -> [CA, FE, 00, 02, 68, 69]
3. The early `?` after mem::take
take 9 -> None, view now 0 long
If you are coming from another language¶
- C.
void skip_first(int **p, size_t *len)is the same function: a pointer to the caller's pointer, plus a pointer to the caller's length, because C carries a slice as two loose variables. Rust's&mut &mut [i32]bundles both halves into one fat pointer, so they cannot drift apart, and advancing past the end panics —&t[1..]on an empty slice — where++*p; --*len;would walk off and wrap the length. Theint **idiom transfers directly; the bookkeeping around it is now the compiler's. - C++.
std::span<int>& spassed by reference and reassigned withs = s.subspan(1)is this shape, andstd::string_view&advanced withremove_prefixis the parser cursor. Aspanpassed by value is the plain&mut [i32]case: the callee can write elements and re-point only its copy. What C++ does not track is whether the caller'sspanoutlives the buffer, or whether a secondspanwrites the same elements meanwhile. Rust answers both at compile time —'ain&mut &'a mut [i32]ties the view to the buffer, and&mutrules out a second writer — so getting either wrong is a compile error rather than a danglingspan. - Python. A list slice
xs[1:]is a copy, andmemoryview(buf)[1:]is a new view object; either way, a function cannot rebind the caller's name, so the idiom is to return the new view (view = skip_first(view)) or keep an index in an object.io.BytesIOis the object that advances:read(2)moves its position, likeRead for &[u8]. Rust can hand the callee the caller's variable itself, which Python has no way to express — and the price is saying, in the signature, which of the two lifetimes each returned reference borrows from. - ABAP. The nearest shape is a field symbol re-assigned by a subroutine to a different part of a table, or an index passed by reference that the caller then uses —
ASSIGN lt_tab[ lv_index ] TO <fs>. Both leave it to the programmer that the table is not changed underneath while the pointer or index is kept.&mut &mut [i32]is the checked version: the caller's view moves, and the compiler rejects any other access to those elements while it is out on loan.
See also¶
- Reborrowing — what
&mut t[1..]is, and why it cannot outlivet - Borrowing something forever — the one-lifetime signature rustc suggested here, as a pattern:
&'a mut Thing<'a>, and why no lint flags it - What
&'a Tclaims — the direction reverses behind&mut - Assignment drops the old value —
mem::replace,mem::takeandmem::swapas the ways to get a value out from behind a&mut stris unsized — the two words in a&[T]or&strslicemethods — the slice API the cursor functions are built fromReadandWrite— the traits whose byte-slice impls advance a&mut &[u8]split_first_mut↗ ·impl Read for &[u8]↗ ·std::mem::take↗- Mutable binding, mutable reference —
&mut &mut Tas the bottom row of a 2 × 2 grid taken twice, beside&mut &T,& &mut Tand&&T
Po polsku¶
fn foo(t: &mut &mut [i32]) to referencja wyłączna do referencji na wycinek (slice) należącej do wywołującego. Wycinek to dwa słowa — adres i długość — a zewnętrzne &mut pozwala funkcji nadpisać obie wartości w zmiennej wywołującego: przesunąć początek widoku, skrócić go. Zwykłe t: &mut [i32] pozwala zapisywać elementy, ale t = &mut t[1..] zmienia wtedy tylko lokalną kopię i wywołujący niczego nie zauważy. Wydłużyć się nie da ani tak, ani tak — do tego potrzebne jest &mut Vec<i32>.
Naiwne *t = &mut t[1..] się nie kompiluje (lifetime may not live long enough): nowe pożyczenie przechodzi przez t, więc żyje tylko tyle co zewnętrzne pożyczenie, a *t musi przechować coś, co żyje tyle co wycinek wywołującego. Rozwiązaniem jest std::mem::take(t) — wyjmuje referencję wywołującego na własność, zostawiając pusty wycinek (&mut [T] implementuje Default), i dopiero ją przycina. Podpowiedź kompilatora &'a mut &'a mut [i32] kompiluje funkcję, ale blokuje zmienną wywołującego do końca jej życia (E0502). Zwracając element, nazwij czas życia wewnętrzny: fn pop_front<'a>(t: &mut &'a mut [i32]) -> Option<&'a mut i32>.
Wersja współdzielona, &mut &[u8] albo &mut &str, jest prostsza, bo &[u8] jest Copy — tak wygląda kursor parsera. Biblioteka standardowa robi dokładnie to samo: impl Read for &[u8] przesuwa wycinek za przeczytane bajty, a impl Write for &mut [u8] używa w środku mem::take(self).
Szukaj po polsku: referencja do referencji · przesuwanie wycinka · kursor parsera w Ruscie · rust &mut &mut [T] · rust mem::take slice advance · rust impl Read for &[u8]