|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
# 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.
|
|
|
|
|
Most of the time, we expect types to have a statically known and positive size.
|
|
|
|
|
This isn't always the case in Rust.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -9,47 +9,80 @@ is not always the case, however.
|
|
|
|
|
|
|
|
|
|
# Dynamically Sized Types (DSTs)
|
|
|
|
|
|
|
|
|
|
Rust in fact supports Dynamically Sized Types (DSTs): types without a statically
|
|
|
|
|
Rust supports Dynamically Sized Types (DSTs): types without a statically
|
|
|
|
|
known size or alignment. On the surface, this is a bit nonsensical: Rust *must*
|
|
|
|
|
know the size and alignment of something in order to correctly work with it! In
|
|
|
|
|
this regard, DSTs are not normal types. Due to their lack of a statically known
|
|
|
|
|
size, these types can only exist behind some kind of pointer. Any pointer to a
|
|
|
|
|
DST consequently becomes a *fat* pointer consisting of the pointer and the
|
|
|
|
|
this regard, DSTs are not normal types. Because they lack a statically known
|
|
|
|
|
size, these types can only exist behind a pointer. Any pointer to a
|
|
|
|
|
DST consequently becomes a *wide* pointer consisting of the pointer and the
|
|
|
|
|
information that "completes" them (more on this below).
|
|
|
|
|
|
|
|
|
|
There are two major DSTs exposed by the language: trait objects, and slices.
|
|
|
|
|
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 reflection
|
|
|
|
|
with a vtable containing all the information necessary to use the type.
|
|
|
|
|
This is the information that completes a trait object: a pointer to its vtable.
|
|
|
|
|
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 or
|
|
|
|
|
`Vec`. The information that completes a slice is just the number of elements
|
|
|
|
|
it points to.
|
|
|
|
|
`Vec`. The information that completes a slice pointer is just the number of elements
|
|
|
|
|
it points to. The runtime size of the pointee is just the statically known size
|
|
|
|
|
of an element multiplied by the number of elements.
|
|
|
|
|
|
|
|
|
|
Structs can actually store a single DST directly as their last field, but this
|
|
|
|
|
makes them a DST as well:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
// Can't be stored on the stack directly
|
|
|
|
|
struct Foo {
|
|
|
|
|
struct MySuperSlice {
|
|
|
|
|
info: u32,
|
|
|
|
|
data: [u8],
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Although such a type is largely useless without a way to construct it. Currently the
|
|
|
|
|
only properly supported way to create a custom DST is by making your type generic
|
|
|
|
|
and performing an *unsizing coercion*:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
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 actually allows types to be specified that occupy no space:
|
|
|
|
|
Rust also allows types to be specified that occupy no space:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
struct Foo; // No fields = no size
|
|
|
|
|
struct Nothing; // No fields = no size
|
|
|
|
|
|
|
|
|
|
// All fields have no size = no size
|
|
|
|
|
struct Baz {
|
|
|
|
|
foo: Foo,
|
|
|
|
|
struct LotsOfNothing {
|
|
|
|
|
foo: Nothing,
|
|
|
|
|
qux: (), // empty tuple has no size
|
|
|
|
|
baz: [u8; 0], // empty array has no size
|
|
|
|
|
}
|
|
|
|
@ -57,13 +90,13 @@ struct Baz {
|
|
|
|
|
|
|
|
|
|
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 realized
|
|
|
|
|
in a generic context: Rust largely understands that any operation that produces
|
|
|
|
|
or stores a ZST can be reduced to a no-op. First off, storing it doesn't even
|
|
|
|
|
make sense -- it doesn't occupy any space. Also there's only one value of that
|
|
|
|
|
type, so anything that loads it can just produce it from the aether -- which is
|
|
|
|
|
in a generic context: Rust largely understands that any operation that produces
|
|
|
|
|
or stores a ZST can be reduced to a no-op. First off, storing it doesn't even
|
|
|
|
|
make sense -- it doesn't occupy any space. Also there's only one value of that
|
|
|
|
|
type, so anything that loads it can just produce it from the aether -- which is
|
|
|
|
|
also a no-op since it doesn't occupy any space.
|
|
|
|
|
|
|
|
|
|
One of the most extreme example's of this is Sets and Maps. Given a
|
|
|
|
|
One of the most extreme examples of this is Sets and Maps. Given a
|
|
|
|
|
`Map<Key, Value>`, it is common to implement a `Set<Key>` as just a thin wrapper
|
|
|
|
|
around `Map<Key, UselessJunk>`. In many languages, this would necessitate
|
|
|
|
|
allocating space for UselessJunk and doing work to store and load UselessJunk
|
|
|
|
@ -78,9 +111,8 @@ support values.
|
|
|
|
|
|
|
|
|
|
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 default in Rust)
|
|
|
|
|
may return `nullptr` when a zero-sized allocation is requested, which is
|
|
|
|
|
indistinguishable from out of memory.
|
|
|
|
|
and standard allocators may return `null` when a zero-sized allocation is
|
|
|
|
|
requested, which is indistinguishable from the out of memory result.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -97,7 +129,7 @@ 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
|
|
|
|
|
an empty type 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
|
|
|
|
@ -125,9 +157,35 @@ But this trick doesn't work yet.
|
|
|
|
|
|
|
|
|
|
One final subtle detail about empty types is that raw pointers to them are
|
|
|
|
|
actually valid to construct, but dereferencing them is Undefined Behavior
|
|
|
|
|
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.
|
|
|
|
|
because 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 because
|
|
|
|
|
Rust doesn't really have any safety guards against trying to instantiate
|
|
|
|
|
empty types with unsafe code, and if you do it, it's Undefined Behaviour.
|
|
|
|
|
This was especially problematic because developers had a habit of converting
|
|
|
|
|
raw pointers to references and `&Void` is *also* Undefined Behaviour to
|
|
|
|
|
construct.
|
|
|
|
|
|
|
|
|
|
`*const ()` (or equivalent) works reasonably well for `void*`, and can be made
|
|
|
|
|
into a reference without any safety problems. It still doesn't prevent you from
|
|
|
|
|
trying to read or write values, but at least it compiles to a no-op instead
|
|
|
|
|
of UB.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Extern Types
|
|
|
|
|
|
|
|
|
|
There is [an accepted RFC][extern-types] 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 of
|
|
|
|
|
Rust 2018, the feature is stuck in limbo over how `size_of::<MyExternType>()`
|
|
|
|
|
should behave.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[dst-issue]: https://github.com/rust-lang/rust/issues/26403
|
|
|
|
|
[extern-types]: https://github.com/rust-lang/rfcs/blob/master/text/1861-extern-types.md
|
|
|
|
|