Skip to content

← Smart Pointers and Interior Mutability step 4 of 26

Medium Primitives

`large_enum_variant`: boxing the fat variant

Route a stream of protocol messages.

pub fn route(msgs: Vec<(String, String)>) -> Vec<String>

Each input pair is (kind, payload) and produces one output line:

kind becomes output
"ping" Msg::Ping "ping"
"text" Msg::Text(payload) "text:<payload>"
"blob" Msg::Blob(512 bytes) "blob:<sum of the 512 bytes>"
anything else nothing "drop:<kind>"

The blob’s 512 bytes are the payload’s bytes repeated cyclically, or all zeroes when the payload is empty.

This one starts already working

Read the starter. It compiles. It produces the right answer for every test case. It is also rejected, and the rejection is the entire lesson:

error: large size difference between variants
 --> src/lib.rs:3:1
  |
3 | / pub enum Msg {
4 | |     Ping,
5 | |     Text(String),
  | |     ------------ the second-largest variant contains at least 24 bytes
6 | |     Blob([u8; 512]),
  | |     --------------- the largest variant contains at least 512 bytes
7 | | }
  | |_^ the entire enum is at least 520 bytes

Why that is a real cost and not a style opinion

An enum is laid out as a tag plus enough room for its largest variant, and that size applies to every value of the type. A Msg::Ping carries no data at all, yet in the starter’s layout a Msg::Ping occupies 520 bytes. So does every Msg::Text. A Vec<Msg> of a thousand pings allocates half a megabyte to store a thousand tags.

It gets worse than the memory. Every time a Msg is moved — returned from a function, pushed into a vector, sent down a channel, matched by value — the compiler emits a 520-byte copy, because moves in Rust are memcpy of the whole value. A modern cache line is 64 bytes; a Msg spans nine of them. The small, common messages pay the price of the big, rare one.

The fix the lint suggests is Box<[u8; 512]>. The variant shrinks from 512 bytes to one pointer, the enum shrinks from 520 bytes to 32, and every Ping and Text gets cheap again. You will need Box::new(buf) at the construction site; the match arm needs no change at all, because b.iter() autoderefs straight through the box.

The honest caveat: boxing is not free

Do not walk away thinking “big variant, add Box“. You have traded:

  • Gained a smaller type for every value, which is a saving on every move, every element of every collection, and every cache line.
  • Paid one heap allocation and one pointer indirection every time you construct or read the big variant.

That trade is good when the big variant is rare — an error payload, an occasional bulk frame, a Result‘s Err arm — and it is a pessimisation when the big variant is the common one. If ninety per cent of your messages are blobs, boxing means ninety per cent of your messages now cost a malloc and a pointer chase to save bytes on the ten per cent that do not.

Clippy cannot know your traffic mix. The lint’s threshold is a heuristic, and it is configurable through clippy.toml — which a single-file submission cannot supply, so the built-in default is what grades you here. That is also the honest answer to “is clippy always right”: no, and this is a lint where a comment justifying #[allow(clippy::large_enum_variant)] is a legitimate outcome in a real codebase. It is not the outcome here, because the point of the exercise is to make the layout change.

The same lint, wearing a different hat

result_large_err is large_enum_variant for the specific case that bites hardest: a Result<T, E> whose E is enormous. Since Result is an enum, a 200-byte error type makes every successful return 200 bytes too — on the happy path, which is the path you took ninety-nine per cent of the time. The idiomatic answer is Box<MyBigError> or Box<dyn Error>, which is why so much real Rust returns Result<T, Box<dyn Error>>.

And note which lints do not fire here. box_collection and vec_box exist to stop you boxing things that are already heap-allocated — Box<Vec<u8>>, Vec<Box<i64>>. A [u8; 512] is a genuine inline array, so boxing it is the right call rather than a double indirection. Knowing which side of that line you are on is the skill.