-
General idea: Compile-time-only type arguments that are erased / not monomorphized, just like lifetimes.
-
Syntax:
fn foo<poly T>(…)
. Function is compiled down to a single non-generic ABI / symbol / function. -
Many applications:
- Allows a limited form of generics in dynamic linking.
- Can be used in methods of object-safe traits.
- Saves monomorphization overhead when it’s not needed.
- Allows polymorphic recursion.
- Can be used in
Foo: for<poly T> TraitBound<Bar<T>>
bounds. - Polymorphic arguments are allowed to be left under-specified.
-
Many future possibilities:
- TODO…
-
In a
fn foo<poly T>(…) -> … { … }
function,T
can only be used in limited ways!- as parameter for other functions with
poly T
type argument - lifetime bounds
T: 'a
are okay, most trait bounds are not allowed (more details below TODO, STILL MISSING) - as parameters for types with a
poly T
type argument- standard library types with
poly T
type argument:&'a T
&'a mut T
*const T
*mut T
cell::Ref<'a, poly T>
cell::RefMut<'a, poly T>
PhantomData<poly T>
- some standard library functions/methods with
T: ?Concrete
type argument:- TODO…
- standard library types with
- as parameter for other functions with
-
Structs can have
poly T
type arguments- all usages in fields must restricted like for the usages mentioned above
- the
Drop
implementation must havepoly T
arguments, and the restrictions for functions apply to the implementation offn drop(&mut self)
-
If a
struct Foo<poly T>
is considered, thenFoo<Bar>
andFoo<Baz>
have the same- layout
- drop implementation
yet still, ordinary generic functions
fn baz<T>
can treat them differently, hence need to be monomorphized forbaz::<Foo<Bar>>
andbaz::<Foo<Baz>>
separately; also you cannot call an ordinary generic functionbaz::<Foo<T>>
using the generic polymorphic parameterT
from inside the definition of a functionfn foo<poly T>
. This means there needs to be a way for a generic function to opt in guaranteeing to treat types likeFoo<Bar>
orFoo<Baz>
the same at run-time. These functions support more than just “concrete” types, hence they get a new opt-out trait bound: -
Syntax:
fn foo<T: ?Concrete>(…)
. Function does not differentiate between differentT
s that only differ inpoly
arguments. -
With a struct
Foo<poly T>
, in the body of a functionfoo<poly T>() { … }
, you can passT
toFoo
, building the typeFoo<T>
; but then that type will not be considered to implementConcrete
, so you can only use it as an argument for functions or types that accept a<S: ?Concrete>
type argument. -
?Concrete
types have a known layout, and can be dropped. But not much more beyond that. -
Functions and structs can, of course, have
T: ?Concrete
type arguments, usual trait resulution rules apply to determine what you can use the parameter for.- For structs, their drop implementation – if any is present – must naturally have a
T: ?Concrete
bound, too. - Standard library types with
T: ?Concrete
type argument:Box<T: ?Concrete>
- TODO…
- Some standard library functions/methods with
T: ?Concrete
type argument:- TODO…
- For structs, their drop implementation – if any is present – must naturally have a
-
Each of
poly T
andT: ?Concrete
also supportspoly T: ?Sized
andT: ?Concrete + ?Sized
Overview of how fine-grained monomorphization is for each case
“polymorphic type argument” |
type argument supporting non-“concrete” types… perhaps: “uniform type argument” |
“concrete type argument” | ||
---|---|---|---|---|
|
|
|
| |
Monomorphization |
No monomorphization at all, a single implementation at run time. |
One monomorphized instance per unsized-metadata-layout. E.g. if |
One monomorphized instance for each type |
For each concrete type (i.e. only ignoring lifetime differences). |
Additional Remarks |
For a For guaranteed-drop-glue-free types, e.g. at least By “guaranteed match of layout / no further monomorphization”, I mean – in terms of language semantics – that you won’t get any errors about the compiler recursively trying to instantiate an infinititude of monomorphized instances of your code. Trait objects all have the same layout and drop implementation. |
Some most basic polymorphic recursion example. (TODO: add explanations)
use std::{
alloc::{alloc, dealloc, Layout},
ptr::{self, NonNull},
};
struct PolyBox<poly T> {
ptr: NonNull<T>,
metadata: NonNull<Metadata<T>>,
}
impl<T: ?Concrete> PolyBox<T> {
fn new(x: T) -> Self {
let meta: &Metadata<T> = StaticMetadata::META;
let ptr = unsafe { NonNull::new_unchecked(alloc(meta.layout)) };
let ptr: NonNull<T> = ptr.cast();
unsafe { ptr.as_ptr().write(x) };
Self {
ptr,
metadata: NonNull::from(meta),
}
}
}
impl<poly T> Drop for PolyBox<T> {
fn drop(&mut self) {
let meta: &Metadata<T> = unsafe { self.metadata.as_ref() };
struct DeallocOnDrop(*mut u8, Layout);
let _guard = DeallocOnDrop(self.ptr.as_ptr().cast(), meta.layout);
unsafe { (meta.drop_in_place)(self.ptr.as_mut()) };
impl Drop for DeallocOnDrop {
fn drop(&mut self) {
unsafe { dealloc(self.0, self.1) };
}
}
}
}
struct Metadata<poly T> {
layout: Layout,
drop_in_place: unsafe fn(*mut T),
}
impl<T: ?Concrete> Metadata<T> {
const fn new() -> Self {
Self {
layout: Layout::new::<T>(),
drop_in_place: ptr::drop_in_place::<T>,
}
}
}
trait StaticMetadata<'a>: ?Concrete + Sized + 'a {
const META: &'a Metadata<Self>;
}
impl<'a, T: ?Concrete + 'a> StaticMetadata<'a> for T {
const META: &'a Metadata<Self> = &Metadata::new();
}
struct Tree<poly T>(Count<T>);
enum Count<poly T> {
Once(PolyBox<T>),
Twice(Box<Count<[PolyBox<T>; 2]>>),
}
use Count::*;
impl<poly T> Tree<T> {
// link 2 trees of equal size
fn link(self, other: Tree<T>) -> Option<Tree<T>> {
fn recurse<poly T>(left: Count<T>, right: Count<T>) -> Option<Count<T>> {
Some(match (left, right) {
(Once(l), Once(r)) => Twice(Box::new(Once(PolyBox::new([l, r])))),
(Twice(l), Twice(r)) => Twice(Box::new(recurse(*l, *r)?)),
_ => None?,
})
}
Some(Tree(recurse(self.0, other.0)?))
}
}