You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nomicon/exotic-sizes.md

4.1 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

// All fields have no size = no size
struct Baz {
    foo: Foo,
    qux: (),      // empty tuple has no size
    baz: [u8; 0], // empty array 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 values 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.

Empty Types

Rust also enables types to be declared that cannot even be instantiated. These types can only be talked about at the type level, and never at the value level. Empty types can be declared by specifying an enum with no variants:

enum Void {} // No variants = EMPTY

Empty types are even more marginal than ZSTs. The primary motivating example for Void types is type-level unreachability. For instance, suppose an API needs to return a Result in general, but a specific case actually is infallible. It's actually possible to communicate this at the type level by returning a Result<T, Void>. Consumers of the API can confidently unwrap such a Result knowing that it's statically impossible for this value to be an Err, as this would require providing a value of type Void.

In principle, Rust can do some interesting analyses and optimizations based on this fact. For instance, Result<T, Void> could be represented as just T, because the Err case doesn't actually exist. The following could also compile:

enum Void {}

let res: Result<u32, Void> = Ok(0);

// Err doesn't exist anymore, so Ok is actually irrefutable.
let Ok(num) = res;

But neither of these tricks work today, so all Void types get you today is the ability to be confident that certain situations are statically impossible.

One final subtle detail about empty types is that raw pointers to them are actually valid to construct, but dereferencing them is Undefined Behaviour because that doesn't actually make sense. That is, you could model C's void * type with *const Void, but this doesn't necessarily gain anything over using e.g. *const (), which is safe to randomly dereference.