RFA-411 · Case file with fixtures · Case 383 of 694 · Runtime evidence
size_of_val on a Slice Uses Its Dynamic Length
std::mem::size_of_val measures the referenced value, including runtime metadata for dynamically sized slices. For &[T], the slice value is len times size_of::<T>; adding another reference measures the fat pointer instead.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The function measures the dynamically sized referent described by the reference, so slice length metadata determines how many element sizes contribute.
- First discriminating check
- Distinguish size_of_val(slice) from size_of_val(&slice), then assert the dynamic length-times-element-size relationship.
I passed a slice to size_of_val and expected the size of one element. Another common expectation is the size of the fat reference itself. The function reports neither: it measures the referenced slice value.
The failing fixture uses three u32 values. size_of_val reports twelve bytes.
The reference selects what gets measured
The function accepts &T where T may be dynamically sized. It returns the size of T, not the size of the argument expression used to reach it.
For this call:
let slice: &[u32] = &[1, 2, 3];
let bytes = std::mem::size_of_val(slice);
The inferred T is [u32]. Its runtime length is three, so the value size is:
3 elements * 4 bytes per u32 = 12 bytes
The repaired fixture asserts this length-times-element-size relationship rather than hard-coding only twelve.
One more reference changes the measured type
Calling size_of_val(&slice) is different. Now the outer reference points to a value whose type is &[u32]. That inner value is the fat slice reference itself.
assert_eq!(
std::mem::size_of_val(&slice),
std::mem::size_of::<&[u32]>(),
);
The placement of & changes the question from “how large is the slice data?” to “how large is the reference value?” I write the type beside measurements during debugging because visual syntax alone is easy to misread.
size_of is for statically sized types
std::mem::size_of::<T>() returns a compile-time size for Sized types. I can ask for size_of::<u32>() or size_of::<&[u32]>(), but not size_of::<[u32]>() because a bare slice has no compile-time length.
size_of_val uses the reference's metadata to answer for the particular dynamic value. Another slice of the same element type and length ten produces a different result.
This is one reason dynamically sized values must live behind a pointer-like type.
The result is not allocation size
For a borrowed slice, size_of_val reports the bytes occupied by its elements. It does not reveal allocator bookkeeping, the capacity of a source Vec, padding before or after an allocation, or bytes owned by nested pointers.
A slice of three String values reports the inline size of three String handles. It does not add the heap allocations containing their text.
Likewise, a slice taken from a Vec knows length but not Vec capacity. The spare allocation is not represented in slice metadata.
I call the result shallow value size unless the element type itself contains all data inline.
Zero-sized elements produce zero
A non-empty slice of a zero-sized type has dynamic length but occupies zero element bytes. The length remains important for iteration and destructor behavior even though size_of_val is zero.
This prevents a general inference from byte size to element count. bytes / size_of::<T>() is invalid when the element size is zero and can lose information even in other layouts.
I keep length as the authoritative collection count.
Trait objects use dynamic concrete layout
The Reference on dynamically sized types includes trait objects as another DST family. size_of_val on &dyn Trait uses pointer metadata to determine the size of the dynamic concrete value.
That still remains a shallow layout size. If the concrete value owns heap data, the referenced allocations are not recursively counted.
For memory profiling I use allocation instrumentation or a domain-specific deep-size routine with cycle and sharing rules. size_of_val cannot invent those ownership semantics.
Overflow and layout assumptions
Safe slices cannot have arbitrary impossible lengths; constructing one unsafely requires that total byte size and address range satisfy strict validity conditions. A forged length is not a harmless way to ask size_of_val a hypothetical question.
In safe code, the runtime calculation reflects an already valid referenced value. I do not create invalid raw slices to probe layout.
For serialization, I also avoid assuming native size_of_val equals encoded length. Endianness, padding, schema fields, and variable-length encodings make wire size a different contract.
My measurement inventory
Before using a size result, I name whether I need:
- one element's inline size;
- the dynamic slice's initialized bytes;
- the fat pointer's own size;
- a Vec's capacity allocation;
- recursively owned heap bytes;
- encoded or compressed bytes.
The core principle is that size belongs to a precise type and ownership layer. size_of_val(slice) measures the dynamically sized [T] selected by the reference. It uses runtime length metadata, while size_of_val(&slice) measures the reference value one layer above.