rust/exotic-sizes.md
2015-07-06 18:36:16 -07:00

2.4 KiB

% Exotically Sized Types

Most of the time, we think in terms of types with a fixed, positive size. This is not always the case, however.

Dynamically Sized Types (DSTs)

Rust also supports types without a statically known size. On the surface, this is a bit nonsensical: Rust must know the size of something in order to work with it! DSTs are generally produced as views, or through type-erasure of types that do have a known size. Due to their lack of a statically known size, these types can only exist behind some kind of pointer. They consequently produce a fat pointer consisting of the pointer and the information that completes them.

For instance, the slice type, [T], is some statically unknown number of elements stored contiguously. &[T] consequently consists of a (&T, usize) pair that specifies where the slice starts, and how many elements it contains. Similarly, Trait Objects support interface-oriented type erasure through a (data_ptr, vtable_ptr) pair.

Structs can actually store a single DST directly as their last field, but this makes them a DST as well:

// Can't be stored on the stack directly
struct Foo {
    info: u32,
    data: [u8],
}

NOTE: As of Rust 1.0 struct DSTs are broken if the last field has a variable position based on its alignment.

Zero Sized Types (ZSTs)

Rust actually allows types to be specified that occupy no space:

struct Foo; // No fields = no size
enum Bar; // No variants = no size

// All fields have no size = no size
struct Baz {
    foo: Foo,
    bar: Bar,
    qux: (), // empty tuple has no size
}

On their own, ZSTs are, for obvious reasons, pretty useless. However as with many curious layout choices in Rust, their potential is realized in a generic context.

Rust largely understands that any operation that produces or stores a ZST can be reduced to a no-op. For instance, a HashSet<T> can be effeciently implemented as a thin wrapper around HashMap<T, ()> because all the operations HashMap normally does to store and retrieve keys will be completely stripped in monomorphization.

Similarly Result<(), ()> and Option<()> are effectively just fancy bools.

Safe code need not worry about ZSTs, but unsafe code must be careful about the consequence of types with no size. In particular, pointer offsets are no-ops, and standard allocators (including jemalloc, the one used by Rust) generally consider passing in 0 as Undefined Behaviour.