Exotically Sized Types
Most of the time, we expect types to have a statically known and positive size.This isn’t always the case in Rust.
Dynamically Sized Types (DSTs)
Rust supports Dynamically Sized Types (DSTs): types without a staticallyknown size or alignment. On the surface, this is a bit nonsensical: Rust mustknow the size and alignment of something in order to correctly work with it! Inthis regard, DSTs are not normal types. Because they lack a statically knownsize, these types can only exist behind a pointer. Any pointer to aDST consequently becomes a wide pointer consisting of the pointer and theinformation that “completes” them (more on this below).
There are two major DSTs exposed by the language:
- trait objects:
dyn MyTrait
- slices:
[T]
,str
, and others
A trait object represents some type that implements the traits it specifies.The exact original type is erased in favor of runtime reflectionwith a vtable containing all the information necessary to use the type.The information that completes a trait object pointer is the vtable pointer.The runtime size of the pointee can be dynamically requested from the vtable.
A slice is simply a view into some contiguous storage — typically an array orVec
. The information that completes a slice pointer is just the number of elementsit points to. The runtime size of the pointee is just the statically known sizeof an element multiplied by the number of elements.
Structs can actually store a single DST directly as their last field, but thismakes them a DST as well:
// Can't be stored on the stack directly
struct MySuperSlice {
info: u32,
data: [u8],
}
Although such a type is largely useless without a way to construct it. Currently theonly properly supported way to create a custom DST is by making your type genericand performing an unsizing coercion:
struct MySuperSliceable<T: ?Sized> {
info: u32,
data: T
}
fn main() {
let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
info: 17,
data: [0; 8],
};
let dynamic: &MySuperSliceable<[u8]> = &sized;
// prints: "17 [0, 0, 0, 0, 0, 0, 0, 0]"
println!("{} {:?}", dynamic.info, &dynamic.data);
}
(Yes, custom DSTs are a largely half-baked feature for now.)
Zero Sized Types (ZSTs)
Rust also allows types to be specified that occupy no space:
struct Nothing; // No fields = no size
// All fields have no size = no size
struct LotsOfNothing {
foo: Nothing,
qux: (), // empty tuple has no size
baz: [u8; 0], // empty array has no size
}
On their own, Zero Sized Types (ZSTs) are, for obvious reasons, pretty useless.However as with many curious layout choices in Rust, their potential is realizedin a generic context: Rust largely understands that any operation that producesor stores a ZST can be reduced to a no-op. First off, storing it doesn’t evenmake sense — it doesn’t occupy any space. Also there’s only one value of thattype, so anything that loads it can just produce it from the aether — which isalso a no-op since it doesn’t occupy any space.
One of the most extreme examples of this is Sets and Maps. Given aMap<Key, Value>
, it is common to implement a Set<Key>
as just a thin wrapperaround Map<Key, UselessJunk>
. In many languages, this would necessitateallocating space for UselessJunk and doing work to store and load UselessJunkonly to discard it. Proving this unnecessary would be a difficult analysis forthe compiler.
However in Rust, we can just say that Set<Key> = Map<Key, ()>
. Now Ruststatically knows that every load and store is useless, and no allocation has anysize. The result is that the monomorphized code is basically a customimplementation of a HashSet with none of the overhead that HashMap would have tosupport values.
Safe code need not worry about ZSTs, but unsafe code must be careful about theconsequence of types with no size. In particular, pointer offsets are no-ops,and standard allocators may return null
when a zero-sized allocation isrequested, which is indistinguishable from the out of memory result.
Empty Types
Rust also enables types to be declared that cannot even be instantiated. Thesetypes 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 foran empty type is type-level unreachability. For instance, suppose an API needs toreturn a Result in general, but a specific case actually is infallible. It’sactually possible to communicate this at the type level by returning aResult<T, Void>
. Consumers of the API can confidently unwrap such a Resultknowing that it’s statically impossible for this value to be an Err
, asthis would require providing a value of type Void
.
In principle, Rust can do some interesting analyses and optimizations basedon this fact. For instance, Result<T, Void>
is represented as just T
,because the Err
case doesn’t actually exist (strictly speaking, this is onlyan optimization that is not guaranteed, so for example transmuting one into theother is still UB).
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 this trick doesn’t work yet.
One final subtle detail about empty types is that raw pointers to them areactually valid to construct, but dereferencing them is Undefined Behaviorbecause that wouldn’t make sense.
We recommend against modelling C’s void*
type with *const Void
.A lot of people started doing that but quickly ran into trouble becauseRust doesn’t really have any safety guards against trying to instantiateempty types with unsafe code, and if you do it, it’s Undefined Behaviour.This was especially problematic because developers had a habit of convertingraw pointers to references and &Void
is also Undefined Behaviour toconstruct.
*const ()
(or equivalent) works reasonably well for void*
, and can be madeinto a reference without any safety problems. It still doesn’t prevent you fromtrying to read or write values, but at least it compiles to a no-op insteadof UB.
Extern Types
There is an accepted RFC to add proper types with an unknown size,called extern types, which would let Rust developers model things like C’s void*
and other “declared but never defined” types more accurately. However as ofRust 2018, the feature is stuck in limbo over how size_of::<MyExternType>()
should behave.