Returned by value: the caller needs a size¶
Level: 201 · working knowledge · companions: every error · lints · reading
One line: A function that returns by value hands back the value itself, not its address, into room the caller set aside before the call. The caller is compiled once, so that room needs one size. str, [T] and dyn Trait don't have one. That is why -> str is refused, and why str can never be Clone.
fn first_word(s: &str) -> &str { // 2 words come back: an address and a length
s.split(' ').next().unwrap_or("")
}
fn shout(s: &str) -> String { // 3 words come back; the text stays on the heap
s.to_uppercase()
}
// fn trimmed(s: &str) -> str { … } // E0277: the return type needs a size known at compile time
Who reserves the room¶
The caller's machine code for let w = shout("hi"); is fixed at compile time, long before shout runs. It sets aside room for w, and the room is size_of::<String>(): 3 words, whatever the text turns out to be. shout then fills that room. Nothing runs the function first to find out how big its answer is.
So each return type needs one size, known when the caller is compiled:
Verified output of returned_by_value.rs — regenerated by tools/run_examples.py, never hand-typed.
1. What comes back from each signature
-> usize 1 word the number itself: 7
-> &str 2 words an address and a length: "hello"
-> Box<str> 2 words an address and a length: "hello"
-> String 3 words address, capacity, length: "hello"
-> [u8; 1000] 1000 bytes every byte comes back: t[999] = 7
-> str no size size_of_val("hi") = 2, size_of_val("hello") = 5
2. By value hands back a copy; by reference hands back the address
by value: the copy is somewhere else: true
copy = [100, 2, 3], original still [1, 2, 3]
by reference: it points at the original: true
3. A returned String brings back its header, not its text
the text is where build() put it: true
4. Clone returns Self by value, so a str cannot be Clone
name.clone() : a &str, same address and length: true
name.to_owned() : a String, 3 words, text copied: true
Part 1 is the rule. Each of the first five rows is one number for every value of the type. The str row has two values of one type with two sizes, "hi" and "hello", so there is no number for the caller to reserve (str is unsized). Every return type carries the Sized bound you never typed, the same one every generic T does (Some types have no size).
Parts 2 and 3 separate by value from copies everything. By value means the value's own bytes come back, not an address. For an array those bytes are the elements. For a String they are the three-word header: the text stays in its heap buffer, the same way it did when the String went in as an argument in The call stack's practice. Part 4 is why str is not Clone, below.
What the machine does with it¶
There are two ways to hand the value back: in registers, or written into the caller's reserved room through an address the caller passes in as a hidden first argument. Which one a type gets is not simply its size. These eight functions, compiled to LLVM IR (LLVM and its IR explains the format):
#[unsafe(no_mangle)]
#[inline(never)]
pub fn number() -> u64 {
7
}
#[unsafe(no_mangle)]
#[inline(never)]
pub fn two_numbers() -> (u64, u64) {
(7, 8)
}
#[unsafe(no_mangle)]
#[inline(never)]
pub fn text() -> &'static str {
"hello"
}
#[unsafe(no_mangle)]
#[inline(never)]
pub fn two_array() -> [u64; 2] {
[7, 8]
}
#[unsafe(no_mangle)]
#[inline(never)]
pub fn three_numbers() -> (u64, u64, u64) {
(7, 8, 9)
}
#[unsafe(no_mangle)]
#[inline(never)]
pub fn owned() -> String {
String::from("hello")
}
#[unsafe(no_mangle)]
#[inline(never)]
pub fn bytes() -> [u8; 1000] {
[7; 1000]
}
#[unsafe(no_mangle)]
pub fn caller() -> u64 {
let (a, b, c) = three_numbers();
a + b + c
}
define void @bytes(ptr sret([1000 x i8]) align 1 %_0) unnamed_addr #3 {
define i64 @caller() unnamed_addr #4 {
define i64 @number() unnamed_addr #3 {
define void @owned(ptr sret([24 x i8]) align 8 %_0) unnamed_addr #3 {
define { ptr, i64 } @text() unnamed_addr #3 {
define void @three_numbers(ptr sret([24 x i8]) align 8 %_0) unnamed_addr #3 {
define void @two_array(ptr sret([16 x i8]) align 8 %_0) unnamed_addr #3 {
define { i64, i64 } @two_numbers() unnamed_addr #3 {
| Signature | Its IR return | How it comes back |
|---|---|---|
-> u64 |
i64 |
one register |
-> (u64, u64) |
{ i64, i64 } |
two registers |
-> &'static str |
{ ptr, i64 } |
two registers: the address, then the length |
-> [u64; 2] |
void, and a parameter ptr sret([16 x i8]) |
written through a pointer to 16 bytes the caller reserved, although it is two words like the row above |
-> (u64, u64, u64) |
void, and ptr sret([24 x i8]) |
the same, 24 bytes |
-> String |
void, and ptr sret([24 x i8]) |
the same: a String header is 24 bytes here |
-> [u8; 1000] |
void, and ptr sret([1000 x i8]) |
the same, 1000 bytes |
One number or a pair of them came back in registers; the two-word array did not, and neither did anything bigger. sret is LLVM's name for the hidden argument: structure return (drawn here). Either way the size is written into the function's own signature — i64, { ptr, i64 }, [1000 x i8]. For -> str there would be no number to write there.
The assembly shows both halves. caller reserves the room and passes its address in rdi. three_numbers writes through rdi and never allocates anything:
rustc --edition 2024 --crate-type lib -C opt-level=1 -C debuginfo=0 --emit asm -C llvm-args=-x86-asm-syntax=intel returns.rs
_caller:
push rbp
mov rbp, rsp
sub rsp, 32
lea rdi, [rbp - 24]
call _three_numbers
mov rax, qword ptr [rbp - 16]
add rax, qword ptr [rbp - 24]
add rax, qword ptr [rbp - 8]
add rsp, 32
pop rbp
ret
_three_numbers:
push rbp
mov rbp, rsp
mov rax, rdi
mov qword ptr [rdi], 7
mov qword ptr [rdi + 8], 8
mov qword ptr [rdi + 16], 9
pop rbp
ret
_bytes:
push rbp
mov rbp, rsp
push rbx
push rax
mov rbx, rdi
mov edx, 1000
mov esi, 7
call _memset
mov rax, rbx
add rsp, 8
pop rbx
pop rbp
ret
sub rsp, 32thenlea rdi, [rbp - 24]: the caller makes room on its own stack frame and passes the address of that room.mov qword ptr [rdi + 16], 9: the callee's answer goes directly into the caller's frame.mov edx, 1000: inbytesthe size is a constant in the machine code, the lengthmemsetgets. Astrhas no such constant.
Where the callee builds the value depends on the build. A function that fills a local [u64; 64] and returns it gets, at opt-level=0, a 512-byte alloca for the local and a memcpy of 512 bytes into the sret room at the end. At opt-level=1 and 3 the sret parameter itself is named %t, the local: the array is built in the caller's room and nothing is copied. (rustc 1.98.0, x86_64-apple-darwin, LLVM IR.) Either way the value ends in room the caller reserved.
This is what rustc 1.98.0 chose for this target. The Reference says of Rust's own calling convention that it "offers no stability guarantees" (ABI ↗), so where the line between registers and sret falls may change. A function marked extern "C" follows the platform's published C rules instead. The need for a size does not change, because both ways need one before the call: a register has a fixed width, and reserved room has a length.
So -> str has no signature¶
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> return_str.rs:1:27
|
1 | fn first_word(s: &str) -> str {
| ^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: the return type of a function must have a statically known size
(The body in that file was *s, which is a second error, E0507; every error shows both.)
The same is true of every unsized type, and the fix is always the same: return something sized that leads to the unsized value.
| Instead of | Return | What comes back | Who owns the bytes |
|---|---|---|---|
-> str |
&str |
2 words | the caller's data, borrowed: it must outlive the result |
-> str |
String |
3 words | the result, in a new heap buffer that can grow |
-> str |
Box<str> |
2 words | the result, in a heap buffer of exactly that length |
-> [T] |
&[T], Vec<T>, Box<[T]> |
2, 3, 2 words | as the three rows above |
-> dyn Trait |
impl Trait |
the concrete type's size | the result, and the compiler knows which type |
-> dyn Trait |
Box<dyn Trait> |
2 words: address and vtable | the result, on the heap |
Parameters are the same question in the other direction. fn count(s: str) is the same E0277, and there rustc suggests the fix itself: "function arguments must have a statically known size, borrowed types always have a known size", with &str as the help.
Why str is not Clone¶
-> Self is a return by value. A str could never be handed back that way, and std writes the requirement onto the trait line itself as the supertrait : Sized. So there is no impl Clone for str, and there can't be (Step 4 of the ToOwned path).
What name.clone() on a &str does instead is part 4 of the output above. The call finds the reference's own Clone, which copies the two words (Reading the Clone for &T hover reads that impl line by line). rustc flags it:
warning: call to `.clone()` on a reference in this situation does nothing
--> clone_str.rs:3:20
|
3 | let copy = name.clone();
| ^^^^^^^^ help: remove this redundant call
|
= note: the type `str` does not implement `Clone`, so calling `clone` on `&str` copies the reference, which does not do anything and can be removed
= note: `#[warn(noop_method_call)]` on by default
ToOwned exists for this gap. Its method returns Self::Owned, a different type that is sized, so str can implement it and hand back a String (ToOwned is Clone with a separate owned type).
A common explanation, checked¶
One explanation of that warning reads: "The warning explains it: str does not implement Clone, because it's unsized and can't be returned by value. So there's no str::clone for auto-deref to find. The only clone in scope is the one on the reference type." Four claims, run:
| Claim | Verdict | What rustc 1.98.0 shows |
|---|---|---|
| "The warning explains it" | half | The warning's note says str does not implement Clone. It gives no reason. The reason is in Clone's declaration, pub trait Clone: Sized. |
"str is not Clone because it is unsized and can't be returned by value" |
true | clone returns Self by value, and -> str is E0277 with "the return type of a function must have a statically known size". |
"no str::clone for auto-deref to find" |
right conclusion, wrong mechanism | No dereference is involved. For name: &str the first receiver type tried is &str itself, which is exactly str::clone's receiver type (&self on str). Had it existed, it would have matched first. The reference's clone is found one step later, at &&str, by adding a &, not by removing one. The dot takes the first receiver that fits walks the list. |
"the only clone in scope is the one on the reference type" |
loose | In scope is about which traits are imported, and Clone is in the prelude either way. The accurate version: it is the only clone method lookup can find. |
If you are coming from another language¶
C. The same machinery, with one hole Rust does not have. A C function cannot return an array at all: int make(void)[3]; is "function cannot return array type 'int[3]'" (Apple clang 21), and the workaround is to wrap it, struct Triple { int v[3]; }, which comes back whole. Rust's -> [u8; 1000] needs no wrapper, because a Rust array keeps its length in its type; a C array passed to a function decays to a pointer and loses it (the C library on that decay ↗). C has no unsized return either: a char * is the &str of C without the length. And return &local; is a warning C lets you build past, where Rust stops with E0515 (A stack slot is reused).
C++. C++17 made copy elision part of the language: returning a prvalue constructs it straight in the caller's room. A type with its copy and move constructors deleted can be returned from Pinned make() { return Pinned(); } under -std=c++17 and is "call to deleted constructor of 'Pinned'" under -std=c++14 (Apple clang 21). Rust promises nothing of the kind, and needs no promise: a move is a byte copy that runs no user code, so whether rustc builds in place (it does at opt-level=1, above) changes speed, never behaviour.
Python. Nothing is returned by value. return result hands back a reference to an object on the heap, so the caller's name and any other name for that object see the same changes. The size question never comes up, which is also why Python needs "a buffer the caller owns and the callee may write into" spelled out by hand for readinto and recv_into (the Python library on bytearray ↗): that is the sret pattern, written as an argument.
Go. Structs are returned by value, as here. The difference is returning an address: p := Point{1, 2}; return &p compiles, and go build -gcflags=-m reports "moved to heap: p" (Go 1.25.5) — escape analysis quietly promotes the local to the heap. Rust never moves a value to the heap behind your back, so the same line is E0515, and the fix is the one this page is about: return the Point itself.
Java. Primitives come back by value; everything else is a reference, as in Python — a returned int[] is the same array the callee kept. The JVM decides where objects live, so there is no unsized-return error to meet.
Practice¶
Three ways around -> str. fn trimmed(s: &str) -> str { … } is refused. Write it three times, returning &str, String and Box<str>. For each, print how many words came back, and whether the result points into the input's bytes. Then drop the input: which results can you still print, and what does rustc say about the one you can't?
Solution
returned_by_value_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: `fn trimmed(s: &str) -> str` is refused. Three signatures
//! that compile, and what each one hands back.
//!
//! Every answer is a size in words or a comparison of addresses, never an
//! address, so the answer key is the same on every run and every target.
//!
//! rustc --edition 2024 returned_by_value_kata.rs -o /tmp/rbvk && /tmp/rbvk
use std::mem::size_of;
fn words<T>() -> usize {
size_of::<T>() / size_of::<usize>()
}
/// Borrows: the result is a window onto the caller's text.
fn trimmed_borrowed(s: &str) -> &str {
s.trim()
}
/// Owns, with room to grow: the text is copied into a new buffer.
fn trimmed_string(s: &str) -> String {
s.trim().to_string()
}
/// Owns, fixed length: the text is copied, and the capacity word is gone.
fn trimmed_boxed(s: &str) -> Box<str> {
s.trim().into()
}
/// Does `part` start inside `whole`'s bytes?
fn points_into(part: &str, whole: &str) -> bool {
whole.as_bytes().as_ptr_range().contains(&part.as_ptr())
}
fn main() {
let input = String::from(" hello ");
let a = trimmed_borrowed(&input);
let b = trimmed_string(&input);
let c = trimmed_boxed(&input);
println!("input {input:?}");
println!(" -> &str {} words {a:?} points into the input: {}", words::<&str>(), points_into(a, &input));
println!(" -> String {} words {b:?} points into the input: {}", words::<String>(), points_into(&b, &input));
println!(" -> Box<str> {} words {c:?} points into the input: {}", words::<Box<str>>(), points_into(&c, &input));
drop(input);
println!("after drop(input), b and c still work: {b} {c}");
// println!("{a}"); // E0505: cannot move out of `input` because it is borrowed
}
Verified output of returned_by_value_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
input " hello "
-> &str 2 words "hello" points into the input: true
-> String 3 words "hello" points into the input: false
-> Box<str> 2 words "hello" points into the input: false
after drop(input), b and c still work: hello hello
The &str is the only one that points into the input, so it is the only one the drop ends. Uncomment the last line and it is E0505, "cannot move out of input because it is borrowed". The two owned results brought their own bytes.
See also¶
Around it in this section:
- The call stack: the region a call reserves, and the return slot a value moves out through
- A stack slot is reused: why
-> &strpointing at a local isE0515, and returning theStringby value is the fix - Stack and heap: why a
Stringis 24 bytes whether it holds 5 characters or 5,000 - What an address shows: a move copies the header and relocates no text, the same thing part 3 shows for a return
- Ownership and moves: a returned value moves to the caller, which now owns it
- Drawing
sret: the hidden pointer drawn in three panels, written out by hand, and watched in lldb
The unsized types:
stris unsized and Some types have no size- Wide pointers: the second word a
&strorBox<dyn Trait>carries Box<str>and Returning a trait: two of the fixes in the tableClonehands backSelf,CopyvsCloneandToOwned- Reading the
Clone for &Thover: the impl.clone()on a&strlands on, as the editor shows it
Underneath:
- Functions: the tail expression that is the value handed back
- LLVM and its IR: reading the
definelines above - What Rust: The Practical Guide says about ownership in functions, run: a book's
-> &Vec<i32>returning a local,E0106and thenE0515, and the heap bytes coming back untouched when theVecis returned by value
Companions: every error · lints · reading
Po polsku¶
Zwracanie przez wartość (returned by value) znaczy, że funkcja oddaje samą wartość, a nie jej adres. Wywołujący rezerwuje na nią miejsce jeszcze przed wywołaniem, a jego kod jest kompilowany raz, więc potrzebuje jednej liczby bajtów. str, [T] i dyn Trait takiej liczby nie mają: "hi" to 2 bajty, "hello" 5, a typ jest ten sam. Stąd -> str to błąd E0277.
Na maszynie widać to wprost. Do dwóch słów wraca w rejestrach (&str to adres i długość). Coś większego callee zapisuje pod adres miejsca zarezerwowanego przez wywołującego, przekazany jako ukryty argument (sret w LLVM IR, rdi na x86-64). Rozmiar jest zapisany w sygnaturze funkcji, np. sret([24 x i8]) dla String.
„Przez wartość” nie znaczy „kopiuje wszystko”: String oddaje trzy słowa nagłówka, a tekst zostaje w tym samym buforze na stercie. Rozwiązania zamiast -> str: &str (pożyczone), String albo Box<str> (własne). Zamiast -> dyn Trait: impl Trait albo Box<dyn Trait>.
To samo tłumaczy, czemu str nie jest Clone. fn clone(&self) -> Self zwraca Self przez wartość, a Clone: Sized zapisuje ten wymóg w deklaracji traitu. .clone() na &str kopiuje więc referencję (ostrzeżenie noop_method_call), a String daje to_owned().
Szukaj po polsku: zwracanie przez wartość · typ o nieznanym rozmiarze · rust return str by value · rust sret · the size for values of type str cannot be known at compilation time · rust calling convention return value