Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 9 of 20

Medium Primitives

Giving ownership back

Two small functions that both have to hand ownership back to their caller.

pub fn split_owned(text: String, sep: char) -> (Vec<String>, usize)
pub fn first_owned(text: String) -> String
  • split_owned splits text on sep and returns the owned pieces together with the number of characters in the original text. ("a,b,c", ',') gives (["a", "b", "c"], 5). Splitting "" gives ([""], 0) — one empty piece, because str::split always yields at least one.
  • first_owned returns the first whitespace-separated word as an owned String, or "" when there isn’t one.

Part 1: the tuple-return ceremony

The fixed helper eats its input:

fn shatter(text: String, sep: char) -> Vec<String>

so the naive body

let pieces = shatter(text, sep);
(pieces, text.chars().count())          // E0382

fails. The repair is one line moved: measure first, hand over second. That is the whole of part 1, and it is worth doing precisely because it is annoying. The Rust Book introduces the same shape early —

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)                     // give it back, plus what you learned
}

— and then says, in effect, this is unbearable, here is &. It is unbearable. Feel it now. When Track 3 lets you write fn calculate_length(s: &String) -> usize you will know exactly what problem the ampersand solves, rather than treating it as the syntax everyone happens to use.

Part 2: the error with no lifetimes in it

first_owned looks like it should be one line:

fn first_slice(text: String) -> &str {
    text.split_whitespace().next().unwrap_or("")
}

That does not compile, and the two errors you get on the way are the first time the compiler reasons about lifetimes with no lifetime syntax anywhere in sight.

The first is E0106:

error[E0106]: missing lifetime specifier
  |
  | fn first_slice(text: String) -> &str {
  |                                 ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
          but there is no value for it to be borrowed from

Read the help line literally. A &str is a pointer into somebody else’s bytes. The signature promises to return one, and the compiler is asking a reasonable question: whose bytes? Not the caller’s — the only String in scope is text, which this function owns.

Follow rustc’s first suggestion and write -> &'static str, and you get the error the suggestion was warning you about:

error[E0515]: cannot return value referencing function parameter `text`
  |
  |     text.split_whitespace().next().unwrap_or("")
  |     ----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |     |
  |     returns a value referencing data owned by the current function
  |     `text` is borrowed here

text is a local. It dies at the closing brace. A reference into it would outlive the bytes it points at, and that is precisely the dangling pointer that ownership exists to prevent. E0515 is the compiler catching a use-after-free before it exists.

::: question So what makes -> String work, when -> &str cannot? Because a String owns its bytes, and ownership can be handed out of a function. Moving a String out is a return: the three stack words travel to the caller, and the heap buffer they point at is untouched and now belongs to whoever received them. Nothing dangles, because nothing was left behind.

.to_string() on the &str is the operation that turns “a view into bytes that are about to die” into “a fresh buffer that outlives them”. It costs one allocation and a copy of the word. That is the price of returning owned data, and for a function like this it is the right price to pay.

The alternative — returning a borrow that stays valid — requires the caller to own the text and hand you a reference to it. That is fn first(text: &str) -> &str, which does compile, and which is Track 8’s subject. Notice that the signature is what changes, not the body: whether a borrow can escape a function is decided entirely by where the data lives. :::

Watch out

The hidden cases include multi-byte text on both functions. Anything that indexes into a str by byte offset will either produce wrong output or panic on a character boundary — chars().count() and split_whitespace() are both UTF-8-aware and do the right thing. And split_owned‘s count is over the original text, separators included, not over the pieces.

Two clippy lints hover around this shape and are worth knowing by name. let_and_return fires when you bind a value and immediately return it in the next statement; needless_late_init fires when you declare a binding and only initialise it later. Both push you toward writing the value where it is produced.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…