Skip to content

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

Easy Primitives

Stack, heap, and what a variable actually holds

Report the shape of five types, measured in machine words.

pub fn layout_report() -> Vec<usize>

Nearly every ownership confusion is really a mental-model confusion about where the bytes live. Once you can draw a String as three words on the stack pointing at a heap buffer, let s2 = s1; stops being a rule and becomes a picture with an obvious problem in it.

The picture

  stack                          heap
  ┌───────────────┐
  │ ptr   ────────┼──────────▶  ┌───┬───┬───┬───┬───┐
  │ len   = 5     │             │ h │ e │ l │ l │ o │
  │ cap   = 8     │             └───┴───┴───┴───┴───┘
  └───────────────┘                              (3 bytes spare)
      a String

A String value is those three words. The characters are somewhere else entirely, and the String merely knows where. Vec<T> has exactly the same shape. Box<T> is one word — a pointer with no length and no capacity, because a Box holds exactly one T. And &str is two words: a pointer and a length, with no capacity, because a &str does not own its bytes and therefore has no say in how much room they were given.

Now re-read rule 2 of the last problem. When you assign a String, the three stack words are copied. Both variables would point at the same heap buffer, and both would eventually try to free it. Rust’s answer — invalidate the source — is the cheapest possible fix: no allocation, no reference count, no runtime check. Just a note in the compiler’s head that one of the two names is now dead.

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 Option<Box<u8>> is the same size as Box<u8>, else 0

Every entry is a ratio or an equality — never an absolute byte count. That is deliberate. Your code runs on your machine, and a 64-bit target would give 24 bytes for a String while a 32-bit target gives 12. Measured in words, both give 3. Writing size assertions in words rather than bytes is a habit worth having: it is the difference between a test that documents a design and a test that documents your laptop.

Entry 4: the niche

Option<T> has to record which of two variants it is, so you would expect it to cost one extra word. For Option<Box<u8>> it costs nothing at all.

The reason is that Box is never null. That makes the all-zeros bit pattern an invalid value for a Box — a niche — and the compiler is free to spend it on None. Some(b) is the pointer; None is all zeros. Same size, no tag. This is also true for Option<&T>, Option<Rc<T>>, Option<NonZeroU32> and several others, and it is why Option::take (which you will meet later in this track) is genuinely free rather than merely cheap.

::: question If the niche trick works for Option<Box<u8>>, does Option<u8> also cost one byte? No — it costs two.

u8 has 256 valid bit patterns and uses all of them. There is no invalid pattern left over for None to occupy, so the compiler falls back to a separate discriminant byte, and alignment does the rest: size_of::<Option<u8>>() == 2.

Niches only exist where a type has values it is not allowed to hold. bool (two valid patterns out of 256) has a huge niche; char has one; &T, Box<T> and NonZero* have exactly one. Plain integers have none. It is a nice example of a validity invariant paying for itself in layout. :::

Two practical notes

size_of has been in the prelude since Rust 1.80, so you do not need use std::mem::size_of;. Writing the import anyway still compiles and still passes this problem’s gate — rustc’s redundant_imports lint is allow-by-default so nothing will tell you — but it is noise, and reviewers will flag it.

Turning a bool into a usize: usize::from(flag) is the direct way and says what it means. flag as usize also works. Prefer the first; as is a blunt instrument you will want to reserve for cases where nothing safer exists.

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

Loading visualization…