We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 2 of 20
Stack, heap, and what a variable actually holds
Report the size of five standard types, measured in machine words.
pub fn layout_report() -> Vec<usize>
A machine word here means size_of::<usize>() bytes: 8 bytes on a 64-bit
target, 4 bytes on a 32-bit one. Thinking about a value's size in words helps
explain what a variable of that type actually holds, and therefore what is
copied when the value is moved.
What a String variable holds
A String stores its text in a separate heap allocation (when it has any
capacity). The String value itself, the part stored in the variable, holds
three words: a pointer to that buffer, the length (how many bytes are in use)
and the capacity (how many bytes the buffer has room for).
the String value heap buffer
┌───────────────┐
│ ptr ────────┼──────────▶ ┌───┬───┬───┬───┬───┬───┬───┬───┐
│ len = 5 │ │ h │ e │ l │ l │ o │ │ │ │
│ cap = 8 │ └───┴───┴───┴───┴───┴───┴───┴───┘
└───────────────┘ (3 bytes unused)
The drawing shows a String holding "hello" in a buffer with room for 8
bytes. The order of the three fields is chosen by the compiler; the drawing's
order is only for illustration. When the String is a local variable, its
three words are typically on the stack, but they could equally sit inside a
struct, a Vec or a Box. Either way, the characters are not part of the
String value; it only records where they are.
The other types in this exercise:
Vec<T>has the same three parts asString: a pointer, a length and a capacity. The standard library documents this forVec.Box<T>, for a sizedTsuch asu8, is a single pointer. It needs no length or capacity because it owns exactly oneT. (ABoxof an unsized type such asBox<str>also stores a length, so it is two words.)&stris a borrowed view of UTF-8 bytes stored somewhere else. It holds a pointer and a length, so it is two words. It has no capacity because it does not own the bytes and can never grow them. A pointer or reference that carries extra data like this length is called a fat pointer.
This also explains why let s2 = s1; moves a String instead of copying it.
The assignment copies only the three words, so s1 and s2 would both point at
the same buffer, and both would try to free it when they went out of scope.
Rust instead treats s1 as moved: the compiler rejects any later use of it and
only s2 frees the buffer. The check happens at compile time, so the move
adds no allocation and no reference count.
The task
Return a Vec<usize> with exactly five entries, in this order:
| index | value |
|---|---|
| 0 | size_of::<String>() divided by size_of::<usize>() |
| 1 | size_of::<Vec<u8>>() divided by size_of::<usize>() |
| 2 | size_of::<Box<u8>>() divided by size_of::<usize>() |
| 3 | size_of::<&str>() divided by size_of::<usize>() |
| 4 | 1 if size_of::<Option<Box<u8>>>() equals size_of::<Box<u8>>(), otherwise 0 |
The function takes no input. The tests either check the whole vector or ask
for one entry by index (0 to 4). On the platforms Rust mode runs on, the
expected result is [3, 3, 1, 2, 1].
Every entry is a ratio or a comparison, never a byte count. A String is 24
bytes on a 64-bit target and 12 bytes on a 32-bit target, but 3 words on both,
so an answer computed in words does not depend on the machine running it. The
same idea is useful when you write your own size assertions.
Entry 4: the niche
Option<T> has to record whether it is Some or None. For many types it
does that with an extra tag field, which makes it larger than T. For
Option<Box<u8>> no tag is needed.
A Box pointer is never null, so the all-zero bit pattern is never a valid
Box. An invalid bit pattern that the compiler can reuse is called a
niche. The compiler represents None with the all-zero pattern and
Some(b) with the pointer itself, so Option<Box<u8>> is the same size as
Box<u8>. The Option documentation guarantees this for Box<T>, &T,
&mut T, NonNull<T> and the NonZero integer types, among others. Other
types built on a non-null pointer, such as Rc<T>, get the same size in
practice, but that is current compiler behavior rather than a documented
guarantee.
Check your understanding
If the niche trick works for Option<Box<u8>>, is Option<u8> the same size as u8?
Show answer
Hide answer
No. A u8 is one byte, but Option<u8> is two.
Every one of the 256 bit patterns of a u8 is a valid value, so there is no
spare pattern for None. The compiler adds a separate tag byte instead, and
size_of::<Option<u8>>() is 2.
A niche exists only when a type has bit patterns it is not allowed to hold.
bool uses 2 of a byte's 256 patterns, so it has many spare ones. char has
invalid values too (surrogate code points and anything above 0x10FFFF).
For &T, Box<T> and the NonZero types the spare pattern is zero. Plain
integer types have none, which is why Option<u32> is larger than u32.
Practical notes
size_of has been in the prelude since Rust 1.80, so you do not need
use std::mem::size_of;. Writing that import anyway still compiles and does not
fail this problem's Clippy check, because the redundant_imports lint is
allow-by-default; it is simply unnecessary.
To turn the bool for entry 4 into a usize, usize::from(flag) converts
false to 0 and true to 1. flag as usize gives the same result.
usize::from is often preferred because the standard library implements
From between primitive number types only where no information can be lost,
while as also performs lossy conversions (such as u64 as u8), so a reader
has to check each as more carefully.
Your submission must compile, pass the tests and pass cargo clippy with
warnings treated as errors (-D warnings).
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.