- Feature Name:
sized_hierarchy - Start Date: 2024-09-30
- RFC PR: rust-lang/rfcs#3729
- Rust Issue: rust-lang/rust#0000
All of Rust's types are either sized, which implement the Sized trait and
have a statically known size during compilation, or unsized, which do not
implement the Sized trait and are assumed to have a size which can be computed
at runtime. However, this dichotomy misses two categories of type - types whose
size is unknown during compilation but is a runtime constant, and types whose
size can never be known. Supporting the latter is a prerequisite to unblocking
extern types, which this RFC addresses. Supporting the former is a prerequisite
to stable scalable vector types, which is left for a future RFC.
Rust has the Sized marker trait which indicates that a type's size
is statically known at compilation time. Sized is a trait which is automatically
implemented by the compiler on any type that has a statically known size. All type
parameters have a default bound of Sized and ?Sized syntax can be used to remove
this bound.
There are two functions in the standard library which can be used to get a size,
std::mem::size_of and std::mem::size_of_val:
pub const fn size_of<T>() -> usize {
/* .. */
}
pub const fn size_of_val<T>(val: &T) -> usize
where
T: ?Sized,
{
/* .. */
}Similar functions are std::mem::align_of and
std::mem::align_of_val.
Due to size_of_val::<T>'s T: ?Sized bound, it is expected that the size of a
?Sized type can be computable at runtime, and therefore a T with T: ?Sized
cannot be a type with no size.
In the Rust community, "unsized" and "dynamically sized" are often used
interchangeably to describe any type that does not implement Sized. This is
unsurprising as any type which does not implement Sized is necessarily
"unsized" and currently the only types this description captures are those which
are dynamically sized.
In this RFC, a distinction is made between "unsized" and "dynamically sized" types. Unsized types is used to refer only to those which have no known size/alignment, such as those described by the extern types RFC. Dynamically-sized types describes those types whose size cannot be known statically at compilation time and must be computed at runtime.
Within this RFC, no terminology is introduced to describe all types which do not
implement Sized in the same sense as "unsized" is colloquially used.
Throughout the RFC, the following terminology will be used:
-
"
Traittypes" will be used to refer to those types which implementTraitand all of its supertraits but none of its subtraits. For example, aSizeOfValtype would be a type which implementsSizeOfVal, andPointee, but notSized.[usize]would be referred to as a "SizeOfValtype" -
The bounds on the generic parameters of a function may be referred to simply as the bounds on the function (e.g. "the caller's bounds")
This RFC is co-authored by @davidtwco and @lqd.
This RFC wouldn't have been possible without the reviews and feedback of
@JamieCunliffe, @JacobBramley,
@nikomatsakis and @scottmcm;
@eddyb for the externref future possibility; the expertise of
@compiler-errors on the type system and suggesting the
use of const traits (now, a future possibility);
@fee1-dead of const traits and fixing the ASCII diagrams; and
the authors of all of the prior art for influencing these ideas.
Introducing a hierarchy of Sized traits resolves blockers for other RFCs which
have had significant interest:
Extern types has long been blocked on these types being
neither Sized nor ?Sized (relevant issue).
Extern types are listed as a "nice to have" feature in Rust for Linux's requests
of the Rust project.
RFC #1861 defined that std::mem::size_of_val and std::mem::align_of_val
should not be defined for extern types but not how this should be achieved, and
suggested an initial implementation could panic when called with extern types,
but this is always wrong, and not desirable behavior in keeping with Rust's
values. size_of_val and align_of_val both panic for extern types in
the current implementation. Ideally size_of_val and align_of_val would error
if called with an extern type, but this cannot be expressed in the bounds of
size_of_val and align_of_val and this remains a blocker for extern types.
Furthermore, unsized types can only be the final member of structs as their
size is unknown and this is necessary to calculate the offsets of later fields.
Extern types also cannot be used in Box as Box requires size and alignment
for both allocation and deallocation.
Introducing a hierarchy of Sized traits will enable the backwards-compatible
introduction of a trait which only extern types do not implement and will
therefore enable the bounds of size_of_val and align_of_val to disallow
instantiations with extern types.
Crucially, there are many other features that this RFC does not unblock which this work is a stepping stone towards enabling - see Future Possibilities.
Most types in Rust have a size known at compilation time, such as u32 or
String. However, some types in Rust do not have known sizes.
For example, slices have an unknown length while compiling and are known as dynamically-sized types, their size must be computed at runtime. There are also unsized types with no size whatsoever.
Various parts of Rust depend on knowledge of the size of a type to work, for example:
-
std::mem::size_of_valcomputes the size of a value, and thus cannot accept extern types which have no size, and this should be prevented by the type system -
Rust allows dynamically-sized types to be used as the final field in a struct, but the alignment of the type must be known, which is not the case for extern types
-
Allocation and deallocation of an object with
Boxrequires knowledge of its size and alignment, which extern types do not have -
For a value type to be allocated on the stack, it needs to have constant known size1, which dynamically-sized and unsized types do not have (but sized do)
Rust uses marker traits to indicate the necessary knowledge required to know
the size of a type, if it can be known. There are three traits related to the size
of a type in Rust: Sized, SizeOfVal, and Pointee.
Sized is a subtrait of SizeOfVal, so every type which implements Sized
also implements SizeOfVal. Likewise, SizeOfVal is a subtrait of Pointee.
┌─────────────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────┐ │
│ │ ┌────────────────┐ │ │
│ │ │ Sized │ SizeOfVal │ Pointee │
│ │ │ {type, target} │ {type, target, ptr metadata} │ {*} │
│ │ └────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Sized is implemented on types which require knowledge of only the
type and target platform in order to compute their size. For example, usize
implements Sized as knowing only the type is usize and the target is
aarch64-unknown-linux-gnu then we can know the size is eight bytes, and
likewise with armv7-unknown-linux-gnueabi and a size of four bytes.
SizeOfVal requires more knowledge than Sized to compute the size: it may
additionally require pointer metadata (therefore size_of is not implemented
for SizeOfVal, only size_of_val). For example, [usize] implements
SizeOfVal as knowing the type and target is not sufficient, the number of
elements in the slice must also be known, which requires reading the pointer
metadata.
Pointee is implemented by any type that can be used behind a pointer, which is
to say, every type (put otherwise, these types may or may not be sized at all).
For example, Pointee is therefore implemented on a u32 which is trivially
sized, a [usize] which is dynamically sized, and an extern type (from
rfcs#1861) which has no known size.
All type parameters have an implicit bound of Sized which will be
automatically removed if a Sized, SizeOfVal or Pointee bound is present
instead.
Prior to the introduction of SizeOfVal and Pointee: Sized's implicit bound
could be removed using the ?Sized syntax, which is now equivalent to a
SizeOfVal bound, and will be deprecated in the next edition.
Traits now have an implicit default bound on Self of SizeOfVal.
Introduce new marker traits, SizeOfVal and Pointee, with SizeOfVal being a
supertrait of Sized and Pointee being a supertrait of
SizeOfVal:
┌────────────────┐
│ Sized │
│ {type, target} │
└────────────────┘
│
implies
│
↓
┌──────────────────────────────┐
│ SizeOfVal │
│ {type, target, ptr metadata} │
└──────────────────────────────┘
│
implies
│
↓
┌──────────────────┐
│ Pointee │
│ {*} │
└──────────────────┘
Or, in Rust syntax:
trait Sized: SizeOfVal {}
trait SizeOfVal: Pointee {}
trait Pointee {}It is possible to stabilise these traits independently of one another. This has implications of limiting which bounds a user can write, but there is no technical limitations imposing a required order.
Implementations of the proposed traits are automatically generated by the compiler and cannot be implemented manually:
-
Pointee- Types that which can be used from behind a pointer (they may or may not have a size)
Pointeewill be implemented for:SizeOfValtypesextern types from rfcs#1861- compound types where any of the fields are
Pointee
- Structs containing
Pointeetypes can only havePointeetypes as the final field- In practice, every type will implement
Pointee
- In practice, every type will implement
-
SizeOfVal- Types whose size is computable given pointer metadata, and knowledge of the type, and target platform
SizeOfValis a subtrait ofPointeeSizeOfValwill be implemented for:Sizedtypes- slices
[T]whereTisSized - string slice
str - trait objects
dyn Trait - compound types where any of the fields are
SizeOfVal
- Structs containing
SizeOfValtypes can only haveSizeOfValtypes as the final field
-
Sized- Types whose size is computable given knowledge of the type, target platform and runtime environment.
Sizedis a subtrait ofSizeOfValSizedcontinues to be implemented for:- primitives
iN,uN,fN,char,bool - pointers
*const T,*mut T - function pointers
fn(T, U) -> V - arrays
[T; n] - never type
! - unit tuple
() - closures and generators
- compound types where every element is
Sized - anything else which currently implements
Sized
- primitives
- Types implementing
Sizeddo not require special machinery in the compiler, such asunsized_localsorunsized_fn_params, to be consideredSized
Introducing new automatically implemented traits is backwards-incompatible,
at least if you try to add it as a bound to an existing function23 (and
new auto traits that go unused aren't that useful), but due to being
supertraits of Sized and Sized being a default bound, these
backwards-incompatibilities are avoided for SizeOfVal and Pointee.
As an implementation detail, Pointee could be treated as syntax for expressing
a lack of any bounds on the sizedness of a parameter, rather than being an
additional obligation to be proven.
Relaxing a bound from Sized to SizeOfVal or Pointee is non-breaking as the
calling bound must have either T: Sized or T: ?Sized, and any concrete types
would implement Sized, and in either circumstance, the newly relaxed bounds
would be satisfied4. A parameter bounded by Sized and used as the return
type of a function could not be relaxed as function return types would still
need to implement Sized.
However, it is not backwards compatible to relax the bounds of trait methods5
and it would still be backwards-incompatible to relax the Sized bound on
a trait's associated type6 for the proposed traits.
It is possible further extend this hierarchy in future by adding new traits before, between, or after the traits proposed in this RFC without breaking backwards compatibility, depending on the bounds that would be introduced (see Forward compatibility and migration).
?Sized would be made syntactic sugar for a SizeOfVal bound. A SizeOfVal
bound is equivalent to a ?Sized bound as all values in stable Rust today whose
types do not implement Sized are valid arguments to
std::mem::size_of_val and as such have a size which can be
computed given pointer metadata and knowledge of the type and target platform,
and therefore will implement SizeOfVal. As there are currently no extern types
or other types which would not implement SizeOfVal, every type in stable Rust
today which would satisfy a ?Sized bound would satisfy a SizeOfVal bound.
Edition change: In the current edition, ?Sized will be syntactic sugar for
a SizeOfVal bound. The ?Trait syntax is currently an error for any trait
except Sized, which would continue. In the next edition, any uses of ?Sized
syntax will be rewritten to a SizeOfVal bound. Any other uses of the
?Trait syntax will be removed as part of the migration and the ?Trait syntax
will be prohibited.
A default implicit bound of Sized is added by the compiler to every type
parameter T that does not have an explicit Sized, ?Sized, SizeOfVal or
Pointee bound.
As SizeOfVal and Pointee are not default bounds, there is no equivalent to
?Sized for these traits.
Edition change: In the current edition, new marker traits would not be added to the prelude.
It is necessary to introduce an implicit default bound of SizeOfVal on a trait's
Self type in order to maintain backwards compatibility in the current edition (referred
to as an implicit supertrait hereafter for brevity). Like implicit Sized bounds,
this is omitted if an explicit Sized, SizeOfVal or Pointee bound is present.
Without this implicit supertrait, the below example would no longer compile: needs_drop's
T: ?Sized would be migrated to a SizeOfVal bound which is not guaranteed to
be implemented by Foo.
trait Foo {
fn implementor_needs_dropped() -> bool {
// `fn needs_drop<T: ?Sized>() -> bool`
std::mem::needs_drop::<Self>() // error! `Self: SizeOfVal` is not satisfied
}
}With the implicit supertrait, the above example would be equivalent to the following example, which would compile successfully.
trait Foo: SizeOfVal {
fn implementor_needs_dropped() -> bool {
// `fn needs_drop<T: ?Sized>() -> bool`
std::mem::needs_drop::<Self>() // ok!
}
}For the same reasons that ?Sized is equivalent to SizeOfVal, adding
a SizeOfVal implicit supertrait will not break any existing implementations
of traits as every existing type already implements SizeOfVal.
This implicit supertrait could be relaxed without breaking changes within the standard library and in third party crates:
If the implicit supertrait was strengthened to a Sized supertrait, it would be a
breaking change as that trait could be being implemented on a type which does not
implement Sized - this is true regardless of whether there is an implicit supertrait
and adding a Sized supertrait to a trait without one would be a breaking change today.
If the implicit supertrait was weakened to a Pointee supertrait
then this would not break any existing callers using this trait as a bound - any
parameters bound by this trait must also have either a ?Sized/SizeOfVal bound
or a Sized bound which would ensure any existing uses of size_of_val (or other
functions taking ?Sized/SizeOfVal) continue to compile.
In the below example, if an existing trait Foo's implicit SizeOfVal
supertrait was relaxed to Pointee then its uses would continue
to compile:
trait Foo: Pointee {}
// ^^^^^^^ new!
fn foo<T: Foo>(t: &T) -> usize { size_of_val(t) }
fn foo_unsized<T: SizeOfVal + Foo>(t: &T) -> usize { size_of_val(t) }Once users can write Pointee or SizeOfVal bounds then it is possible for users
to write functions which would no longer compile if the function was relying on the
implicit supertrait of another bounded trait which was then relaxed:
// This only compiled because `Foo: SizeOfVal`, but if that bound were relaxed
// then it would fail to compile.
fn foo<T: Pointee + Foo>(t: &T) -> usize { size_of_val(t) }Implementations of traits in downstream crates would also not be broken when an implicit supertrait is relaxed.
Any existing implementation of a trait will be on a type which implement at least
SizeOfVal, therefore a relaxation of the implicit supertrait to
Pointee will be trivially satisfied by any existing implementor.
struct Local;
impl Foo for Local {} // not broken!In the bodies of trait implementations, the only circumstance in which there
could be a backwards incompatibility due to relaxation of the implicit supertrait
is when the sizedness traits implemented by Self can be observed - there are
three cases which must be considered: in trait implementations, trait definitions
and subtraits.
In trait implementations, Self refers to the specific implementing type, this
could be a concrete type like u32 or it could be a generic parameter in a
blanket impl. In either case, the type is guaranteed to implement Sized
or SizeOfVal as no types which do not implement one of these two traits
currently exist.
impl Foo for u32 {
fn example(t: &Self) -> usize { std::mem::size_of_val(t) }
// `Self` = `u32`, even if `Foo`'s implicit supertrait is relaxed, `u32` still
// implements `SizeOfVal`
}
impl<T> Foo for T {
fn example(t: &Self) -> usize { std::mem::size_of_val(t) }
// `Self` = `T`, even if `Foo`'s implicit supertrait is relaxed, `T` still
// implements `Sized` because of the default bound
}
impl<T: ?Sized> Foo for T {
fn example(t: &Self) -> usize { std::mem::size_of_val(t) }
// `Self` = `T`, even if `Foo`'s implicit supertrait is relaxed, `T` still
// implements `SizeOfVal` because of the default bound
}Trait definitions are unlike trait implementations in that Self in their bodies
always refers to any possible implementor and the only known bounds on that Self
are the supertraits of the trait. A default body of a method can test whether Self
implements any given sizedness trait (e.g. by calling needs_drop::<Self>() as in the
examples at the start of this section). However, trait definitions can be updated when
an implicit supertrait is relaxed so do not pose any risk of breakage.
Like with trait definitions above, a subtrait defined in a downstream crate can
observe the sizedness traits implemented by Self in default bodies. However,
subtraits will also have an implicit SizeOfVal supertrait which would
guarantee that their bodies continue to compile if a supertrait relaxed its implicit
supertrait:
trait Sub: Foo { // equiv to `Sub: Foo + SizeOfVal`
// `fn needs_drop<T: ?Sized>() -> bool`
fn example() -> bool { std::mem::needs_drop::<Self>() } // ok!
}In many of the cases above, relaxation of the supertrait is only guaranteed to be backwards compatible in third party crates while there is no user code using the new traits this proposal introduces.
While SizeOfVal is equivalent to the current ?Sized bound it replaces, it
excludes extern types (which ?Sized by definition cannot), which prevents
size_of_val from being called with extern types from rfcs#1861.
Due to the changes described in Sized bounds (migrating
T: ?Sized to T: SizeOfVal), changing the bound of size_of_val will
not break any existing callers:
pub const fn size_of_val<T>(val: &T) -> usize
where
T: SizeOfVal,
// ^^^^^^^^^ new!
{
/* .. */
}These same changes apply to align_of_val
Pointee types can only be used as the final field in
non-#[repr(transparent)] compound types as the alignment of these types would
need to be known in order to calculate field offsets. Sized types can be used
in compound types with no restrictions. Like Pointee types, SizeOfVal types
can be used in compound types, but only as the last element.
There is a potential performance impact within the trait system to adding
supertraits to Sized, as implementation of these supertraits will need to be
proven whenever a Sized obligation is being proven (and this happens very
frequently, being a default bound). It may be necessary to implement an
optimisation whereby Sized's supertraits are assumed to be implemented and
checking them is skipped - this should be sound as all of these traits are
implemented by the compiler and therefore this property can be guaranteed.
It is not expected that this RFC's additions would result in much churn within the ecosystem. Almost all of the necessary changes would happen automatically during edition migration.
All bounds in the standard library should be re-evaluated during the implementation of this RFC, but bounds in third-party crates need not be.
Pointee types will primarily be used for localised FFI and so is not expected
to be so pervasive throughout Rust codebases to the extent that all existing
?Sized bounds would need to be immediately reconsidered in light of their
addition, even if in many cases these could be relaxed.
If a user of a Pointee type did encounter a bound that needed to be relaxed,
this could be changed in a patch to the relevant crate without breaking
backwards compatibility as-and-when such cases are encountered.
If edition migration were able to attempt migrating each bound to a more relaxed bound and then use the guaranteed-to-work bound as a last resort then this could further minimise any changes required by users.
With these new traits and having established changes to existing bounds which can be made while preserving backwards compatibility, the following changes could be made to the standard library:
-
T: ?SizedbecomesT: SizeOfVal- It is not a breaking change to relax this bound and it prevents types
only implementing
Pointeefrom being used withBox, as these types do not have the necessary size and alignment for allocation/deallocation
-
T: ?SizedbecomesT: Pointee- It is not a breaking change to relax this bound and there's no reason why
any type should not be able to be used with
PhantomData
As part of the implementation of this RFC, each Sized/?Sized bound in
the standard library would need to be reviewed and updated as appropriate.
In the above sections, this proposal argues that..
- ..adding bounds of new automatically implemented supertraits of a default bound..
- ..relaxing a sizedness bound in a free function..
- ..relaxing implicit sizedness supertraits..
..is backwards compatible and that..
- ..relaxing a sizedness bound for a generic parameter used as a return type..
- ..relaxing a sizedness bound in a trait method..
- ..relaxing the bound on an associated type..
..is backwards incompatible.
There is one known breaking change with this approach under the old trait
solver, due to ?Sized introducing a SizeOfVal bound where it did not
previously. The types team reviewed and FCP'd the
experimental addition of the Sized supertraits, with this breaking change. It
is expected to be rare, with a single known occurrence, and is already accepted
by the next trait solver:
trait ParseTokens {
type Output;
}
impl<T: ParseTokens + ?Sized> ParseTokens for Box<T> {
type Output = ();
}
struct Element(<Box<Box<Element>> as ParseTokens>::Output);
impl ParseTokens for Element {
type Output = ();
}The current trait solver has the following behaviour:
Element: SizeOfVal<Box<Box<Element>> as ParseTokens>::Output: SizeOfVal- Normalize associated type, requires
Box<Element>: ParseTokens - Requires
Element: SizeOfValcycle, goes through the non-coinductiveBox<Element>: ParseTokensobligation, resulting in an overflow
Without the changes described in this RFC, there was no Element: SizeOfVal
constraint, as T: ?Sized did not introduce any constraints.
This case was discovered in a crater run in the red-lightning123/hwc repository, which does not appear to be on crates.io or be a dependency of any other packages. It is tracked in issue rust-lang/rust#143830 until the new trait solver is used by default and fixes it. No other issues about this overflow have been opened since the experiment landed on nightly, in June 2025.
Trait hierarchies with a default trait can be extended in three different ways:
-
- e.g.
NewSized: Sized: SizeOfVal: Pointee - This case doesn't correspond to a trait being proposed in this RFC, but is
worth considering for future compatibility, and is equivalent to
const Sizedin *theconst Sizedfuture possibility)
- e.g.
-
After the default trait, in the middle of the hierarchy
- e.g.
Sized: NewSized: SizeOfVal: PointeeorSized: SizeOfVal: NewSized: Pointee - This case is concretely what is being proposed for
SizeOfValin this RFC
- e.g.
-
After the default trait, at the end of the hierarchy
- i.e.
Sized: SizeOfVal: Pointee: NewSized - This case is concretely what is being proposed for
Pointeein this RFC
- i.e.
In addition, for all of the traits proposed: subtraits will not automatically imply the proposed trait in any bounds where the trait is used, e.g.
trait NewTrait: SizeOfVal {}
// Subtractive case (adding a trait bound will not weaken the existing bounds)
struct NewRc<T: NewTrait> {} // equiv to `T: NewTrait + Sized` as today
// Additive case (adding a trait bound can strengthen the existing bounds)
struct NewRc<T: Pointee + NewTrait> {} // equiv to `T: NewTrait + SizeOfVal` as todayIt remains the case with this proposal that if the user wanted T: SizeOfVal
then it would need to be written explicitly.
This is forward compatible with trait bounds which have sizedness supertraits
implying the removal of the default Sized bound (such as in the Adding
only bounds alternative).
Introduction of a new trait, NewSized for example, in the hierarchy before the
default trait (i.e. to the left of Sized) could be one of two scenarios:
NewSizedis only implemented for a kind of type that could not have existed previously and the properties of this kind of sizedness were not previously assumed ofSized- e.g. hypothetically, if there were a hardware feature that worked only with
prime-numbered-sized types and it was necessary to distinguish between
types with this property and types without, then a
PrimeSizedtrait could be introduced left ofSized
- e.g. hypothetically, if there were a hardware feature that worked only with
prime-numbered-sized types and it was necessary to distinguish between
types with this property and types without, then a
NewSizedaims to distinguish between two categories of type that were previously consideredSized- e.g.
const Sizedfrom theconst Sizedfuture possibility, distinguishes between types with a size known at compile-time and a size only known at runtime, both of which were previously assumed to beSized
- e.g.
Of these two possibilities, new traits in the first scenario can be introduced without any migration necessary or risk of introducing backwards incompatibilities. However, the second scenario is both much more realistic and interesting and thus is assumed for the remainder of this section.
To maintain backwards compatibility, the default bound on type parameters
would need to change to NewSized:
// in `std`..
fn depends_on_newsizedness<T: Sized>() {
// Given that `NewSized` partitions existing `Sized` types into two categories,
// it must be possible for this function body to do something that depends on
// the property that `NewSized` has but `Sized` doesn't, but given that this
// is an argument in the abstract, it's impossible to write that body, so this
// comment will need to serve as a substitute
}
// in user code..
fn unaware_caller<T>() {
// A user having written this code, not knowing that `depends_on_newsizedness` exploits
// the property of `Sized` that `NewSized`-ness now represents, would need their default
// bound to change to `NewSized` so as not to break
depends_on_newsizedness::<T>()
}In some instances, NewSized may be an appropriate default bound. In this
circumstance, a simple migration is necessary - see Simple
Migration.
However, in other circumstances, NewSized may be too strict as a default
bound, and retaining it as a default would preclude the use of
types-that-are-Sized-but-not-NewSized from being used with all existing Rust
code, significantly impacting the usability of those types and the feature which
introduced them.
When this is the case, there are three possibilities for migration:
- On the next edition,
Sizedis the default bound andNewSizedbounds are explicitly written only where the user exploited the property thatNewSizedtypes have thatSizedtypes do not- See Ideal Migration
- On the next edition,
Sizedis the default bound and all existingSizedbounds (implicit or explicit) are rewritten asNewSizedfor backwards compatibility - Accept that
NewSizedwill remain the default bound and proceed with the migration described previously whenNewSizedbeing the default bound was the appropriate option- See Simple Migration
┌────────────────────────────────────────────────┐
│ Is `NewSized` is an appropriate default bound? │
└────────────────────────────────────────────────┘
│ │
Yes No
│ ↓
│ ┌──────────────────────────┐
│ │ Is the "ideal migration" │─────────┐
│ │ possible/practical? │ Yes
│ └──────────────────────────┘ ↓
│ │ ┌───────────────────┐
│ No │ "Ideal Migration" │
│ ↓ └───────────────────┘
│ ┌────────────────────────────────┐
│ │ Is the "compromised migration" │──┐
│ │ possible/practical? │ Yes
│ └────────────────────────────────┘ ↓
│ │ ┌─────────────────────────┐
│ No │ "Compromised Migration" │
↓ ↓ └─────────────────────────┘
┌──────────────────────────────────┐
│ "Simple Migration" │
└──────────────────────────────────┘
An ideal migration would result in minimal code changes for users while
permitting maximal usability of the Sized types which do not implement
NewSized.
With this migration strategy, in the current edition, functions would have a
default bound of NewSized:
fn unaware_caller<T: Sized>() {
// ^^^^^^^^ interpreted as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `NewSized`
let _ = std::size_of::<T>(); // (`size_of` depends only on `Sized`, not `NewSized`)
}In the next edition, assuming that the standard library's bounds have been
updated, functions would have a default bound of Sized and any functions which
depended on the previously implicit NewSized-ness of Sized will have been
rewritten with an explicit NewSized bound (and their callers):
fn unaware_caller<T: NewSized>() {
// ^^^^^^^^^^^ rewritten as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `Sized`
let _ = std::size_of::<T>();
}This migration would require that the compiler be able to keep track of whether
predicates are used in proving obligations (i.e. whether the predicate from
NewSized as the default bound is used, or just Sized that it elaborates to).
rustc currently does not keep track of which predicates are used in proving an
obligation.
However, there is additional complexity to this migration in cross-crate contexts:
A crate foo that depends on crate bar may want to perform the edition
migration first, before its dependency. A generic parameter T's default bound
is NewSized on the previous edition, and Sized in the next edition, and
whether or not it is migrated to Sized (no textual change) or NewSized (now
explicitly written) depends on the uses of T.
Concretely, on the current edition, in the below example, x would have a
migration lint, and y would not:
fn x<T>() {
// ^ diagnostic: this parameter has a `NewSized` bound in the current
// edition, but in the next edition, this will change to
// `Sized`, you need to write `NewSized` explicitly to
// not break
std::depends_on_newsizedness::<T>()
}
fn y<T: AsRef<str>>(t: T) {
// ^ no diagnostic: `T`'s body doesn't require `NewSized`, just `Sized`,
// so doesn't need to change
let x = t.as_ref();
}In the next edition, the above example would migrate to:
fn x<T: NewSized>() {
std::depends_on_newsizedness::<T>()
}
fn y<T: AsRef<str>>(t: T) {
let x = t.as_ref();
}When the use of the generic parameter is in instantiating a item from a dependency, then whether the migration lint should be emitted will depend on whether the dependency has been migrated.
Consider the following example, when migrating crate foo, migration of generic
parameter T in functions x and y will depend on whether the generic
parameter of bar::x and bar::y have a NewSized bound or not. As bar
is not migrated, its default bound is NewSized.
// crate `foo`, unmigrated
fn x<T>() {
bar::x::<T>()
}
fn y<T>() {
bar::y::<T>()
}
// crate `bar`, unmigrated
fn x::<T>() {
size_of::<T>()
}
fn y::<T>() {
std::depends_on_newsizedness::<T>()
}Given the default bound of the previous edition, a naive migration approach
would necessarily migrate foo to the strictest bounds. These stricter bounds
would in turn propagate through foo's call graph, and users of the foo
crate, etc:
// crate `foo`, naive migration
fn x<T: NewSized>() {
bar::x::<T>()
}
fn y<T: NewSized>() {
bar::y::<T>()
}An ideal migration would consider the post-migration bounds of the downstream
crate, even if it has not been migrated, which would result in the following
migration of foo:
// crate `foo`, ideal migration
fn x<T>() {
bar::x::<T>()
}
fn y<T: NewSized>() {
bar::y::<T>()
}This introduces a hazard that within unmigrated crate bar, downstream crates
may begin depending on the bounds as determined by the compiler when looking at
the bodies, not the bounds as written. If bar::x were changed to match
the body of bar::y, then its external interface effectively changes even if the
signature does not. Whether or not the migration lint should be applied would
depend on whether the body has changed since the lint was introduced:
error: default `NewSized` bound will become more relaxed in the next edition
--> src/lib.rs:3:6
|
2 | fn x<T>
| - add the `NewSized` explicitly: `: NewSized`
3 | std::depends_on_newsizedness::<T>()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ you depend on the constness of the `NewSized` default bound here
note: in the current edition, the default bound is `NewSized` but will be `Sized` in the next edition
help: if you just changed your function and have started getting this lint, it's possible that downstream
crates have been relying on the previous interpretation of the `Sized` bound, so it may be a breaking
change to have changed the function body in the way that you have
If it is not possible to determine when NewSized would need to be explicitly
written, it would still be possible to add NewSized explicitly everywhere such
that the default bound can remain Sized. With this migration, newly written
functions would accept Sized-but-not-NewSized types.
With this migration strategy, in the current edition, functions would have a
default bound of NewSized:
fn unaware_caller<T: Sized>() {
// ^^^^^^^^ interpreted as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `NewSized`
let _ = std::size_of::<T>();
}In the next edition, functions would have a default bound of Sized and all
existing implicit or explicit Sized bounds would be rewritten as NewSized:
fn unaware_caller<T: NewSized>() {
// ^^^^^^^^^^^ rewritten as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T: NewSized>() {
// ^^^^^^^^^^^ rewritten as `NewSized`
let _ = std::size_of::<T>();
}While technically feasible, this migration is likely not practical given the amount of code that would be changed.
In a simple migration, explicitly-written Sized would be interpreted as
NewSized on the current editions, and rewritten as NewSized on the next
edition.
After a trait has been introduced before the default trait (per the parent section), introducing more traits before the default trait falls into one of two scenarios:
- Before the leftmost trait (i.e. splitting
NewSized)- e.g.
NewNewSized: NewSized: Sized - In this scenario, introducing the new trait would be backwards compatible, but strengthening any existing bounds to it would not without a migration which would be more challenging without a default bound involved - this is the same as with adding a subtrait to any other trait in user code
- e.g.
- Between the leftmost trait and default trait (i.e. splitting
Sizedagain)- e.g.
NewSized: NewNewSized: Sized - In this scenario, the considerations is the same as in Before the default trait
- e.g.
Introducing a new trait in the middle of the hierarchy is backwards compatible. Future possibilities like Custom DSTs suggest additions of new traits within the hierarchy.
Stricter bounds can be relaxed to a new trait in the hierarchy, but more
relaxed bounds cannot be strengthened. For example, for a Sized: NewSized: SizeOfVal, then:
fn needs_sized<T> {}
// ^ can be relaxed to `T: NewSized`
fn needs_sizeofval<T: SizeOfVal> {}
// ^^^^^^^^^^^^ cannot be strengthened to `NewSized`
fn needs_pointee<T: Pointee> {}
// ^^^^^^^^^^ cannot be strengthened to `NewSized`Relaxing a bound to NewSized is not backwards compatible in a handful of
contexts..
- ..in a trait method
- ..if the bound is
Sizedand the bounded parameter is used as the return type - ..if the bound is on an associated type
If NewSized is after the implicit sizedness supertrait then the implicit
sizedness supertrait and other traits after it can be relaxed to NewSized and
supertraits cannot be strengthened to NewSized (per the reasoning in
Implicit SizeOfVal supertraits). If
NewSized is before the implicit sizedness supertrait then supertraits cannot
be strengthened or relaxed to NewTrait.
When a new trait is introduced after a trait in the hierarchy that is currently
the implicit supertrait - for example, NewSized in Sized: NewSized: SizeOfVal: Pointee- then NewSized will either introduce a new distinction
between types that was previously assumed to be true in default trait bodies, or
it won't (depending on the nature of the distinction created by the specific
trait).
If it does, then NewSized will necessarily need to become the new implicit
supertrait to maintain backwards compatibility. Moving the default supertrait in
this way is backwards compatible as this problem is equivalent to introducing
new traits before the default trait.
Like introducing new traits before the default trait, implicit supertraits are
not ideal and a similar migration is possible. Concretely, an implicit
SizeOfVal supertrait is not ideal as it prevents all existing traits to be
implemented for extern types. A migration away from an implicit supertrait
also has three possibilities:
-
An ideal edition migration would result in no implicit supertrait and would explicitly write a default supertrait on only those trait definitions where a default body requires it.
With this migration, in the current edition, traits would have an implicit
SizeOfValsupertrait:trait Foo {} // ^ - an implicit `SizeOfVal` supertrait trait Bar { // ^ - an implicit `SizeOfVal` supertrait fn example() -> bool { std::mem::needs_drop::<Self>() } }
In the next edition, traits would have an explicitly written
SizeOfValsupertrait only if it is necessary for the default bodies of the trait:trait Foo {} // ^ no implicit supertrait trait Bar: SizeOfVal { // ^^^^^^^^^ an explicit `SizeOfVal` supertrait is added fn example() -> bool { std::mem::needs_drop::<Self>() } } trait Qux {} // ^ this new trait added post-migration has no implicit // supertrait
This migration strategy would require the same compiler support as the Ideal Migration for traits before the default trait.
-
A compromised migration would result in no implicit supertrait and would explicitly write a default supertrait everywhere:
In the current edition, traits would have an implicit
SizeOfValsupertrait:trait Foo {} // ^ - an implicit `SizeOfVal` supertrait trait Bar { // ^ - an implicit `SizeOfVal` supertrait fn example() -> bool { std::mem::needs_drop::<Self>() } }
In the next edition, all traits would have an explicitly written
SizeOfValsupertrait:trait Foo: SizeOfVal {} // ^^^^^^^^^ an explicit `SizeOfVal` supertrait is added trait Bar: SizeOfVal { // ^^^^^^^^^ an explicit `SizeOfVal` supertrait is added fn example() -> bool { std::mem::needs_drop::<Self>() } } trait Qux {} // ^ this new trait added post-migration has no implicit // supertrait
-
If no other migration is deemed feasible or practical then it is possible to keep an implicit supertrait and accept the reduced usability of types which do not implement it.
In the current and next editions, traits would have an implicit
SizeOfValsupertrait:trait Foo {} // ^ - an implicit `SizeOfVal` supertrait trait Bar { // ^ - an implicit `SizeOfVal` supertrait fn example() -> bool { std::mem::needs_drop::<Self>() } } trait Qux {} // ^ this new trait added post-migration has an implicit // `SizeOfVal` supertrait
It is not backwards compatible to relax the bound on an associated type, from
type Foo: Sized to type Foo: SizeOfVal, from type Foo: ?Sized/type Foo: SizeOfVal to type Foo: Pointee, or with any additional sizedness traits
introduced in the hierarchy. This limits the utility of the new sizedness traits
as some operations, like a dereference, are implemented as traits with
associated types:
trait /* std::ops::*/ Deref {
type Target: SizeOfVal;
// ^^^^^^^^^ ideally would change to `Pointee`
fn deref(&self) -> &Self::Target;
}If Deref::Target were relaxed to Pointee then this would result in backwards
incompatibility as in the example below:
fn do_stuff<T: Deref>(t: T) -> usize {
std::mem::size_of_val(t.deref())
//~^ error! the trait bound `<T as Deref>::Target: SizeOfVal` is not satisfied
}This is not optimal as it significantly reduces the usability of extern type,
and limits the relaxations to Pointee that can occur in the standard library.
The most promising approach for migration of associated types is the same as
that being considered for other efforts to introduce new automatically
implemented traits, suggested by @lcnr (original
blog). This ideal migration would defer checks until
post-monomorphization in rustc. For example, after Deref::Target is relaxed to
Pointee, bar would normally stop compiling, but instead this would continue
to compile and emit a future compatibility warning:
fn foo<T: Deref>(t: T) -> usize {
std::mem::size_of_val(t.deref())
//~^ warning! `T::Target: SizeOfVal` won't hold in future versions of Rust
}
fn bar<T: Deref>(t: T) -> usize {
std::mem::size_of_val(t) // no warning as `Deref::Target: SizeOfVal` is not needed
}On the next edition, this can stop being a future compatibility warning and we can have migrated users to write a bound on the associated type only when it was required:
fn foo<T: Deref>(t: T) -> usize
where <T as Deref>::Target: SizeOfVal
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added as part of ideal migration
{
std::mem::size_of_val(t.deref()) // okay!
}
fn bar<T: Deref>(t: T) -> usize {
// no migration as `Deref::Target: SizeOfVal` was not needed
std::mem::size_of_val(t)
}If this is not feasible, a compromised migration with more drawbacks, is to
elaborate the existing SizeOfVal bound in user code over a migration, such as:
fn foo<T: Deref>(t: T) -> usize
where <T as Deref>::Target: SizeOfVal
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added as part of compromised migration
{
std::mem::size_of_val(t.deref())
}
fn bar<T: Deref>(t: T) -> usize
where <T as Deref>::Target: SizeOfVal
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added as part of compromised migration
{
std::mem::size_of_val(t)
}This approach is not optimal, however:
-
It results in a lot of churn when migrating, and for cases that may not always be of interest for a given project
-
While the migrated code would keep working, the implicit defaults of the previous edition would be explicitly brought over, even if the new edition defaults have weaker requirements
- This doesn't make
extern typeany more usable with existing code, and in many cases, the explicit bounds introduced would be stricter than required
- This doesn't make
Furthermore, this wouldn't work in the general case with non-sizedness traits (as would be useful for other ongoing RFCs), as it could cause infinite expansion due to recursive bounds:
trait Recur {
type Assoc: Recur;
}
fn foo<T: Recur>()
where
// when elaborated..
T: Move,
T::Assoc: Move,
<T::Assoc as Recur>::Assoc: Move,
<<T::Assoc as Recur>::Assoc as Recur>::Assoc: Move,
...
{}This limitation does not affect sizedness traits as they do not have associated types themselves.
It may be possible to refine this to run probes in the trait solver at migration time, using obligations with relaxed bounds, and to compare the results. This seems hard to make workable in the general case, and could also run into slowness issues depending on the number of combinations of places to check and number of options to try at each one.
If none of the above approaches are deemed feasible, the status quo with regards to relaxation of bounds on associated types could be maintained and this proposal would still be useful, just slightly less so.
All of the same logic as After the default trait, in the middle of the hierarchy applies. Future possibilities like externref suggest additions of new traits at the end of the hierarchy.
-
This is a not-insignificant change to the
Sizedtrait, which has been in the language since 1.0 and is now well-understood -
This RFC's proposal that adding a bound of
Sized,SizeOfVal, orPointeewould remove the defaultSizedbound is a significant change from the current?Sizedmechanism and can be considered confusing.- Typically adding a trait bound does not remove another trait bound, however this RFC argues that this behaviour scales better to hierarchies of traits with default bounds and constness
-
There are some backwards incompatibilities (see summary-of-backwards-incompatibilities)
There are various points of difference to the prior art related to
Sized, which spans almost the entirety of the design space:
-
In contrast to rfcs#709, marker types aren't used to disable
Sizedbecause we are able to introduce traits to do so without backwards compatibility hazards and that feels more appropriate for Rust -
In contrast to rfcs#1524, items are not added to the
Sizedtrait as this wouldn't be sufficient to capture the range in sizedness that this RFC aims to capture, even if it theoretically could enable Custom DSTs -
In contrast to rfcs#1993, rust#44469, rust#46108, rfcs#2984 and eRFC: Minimal Custom DSTs via Extern Type (DynSized), none of the traits proposed in this RFC are default bounds and therefore do not require additional relaxed bounds be accepted (i.e. no
?SizeOfVal), which has had mixed reception in previous RFCs (rfcs#2255 summarizes these discussions) -
In contrast to rfcs#1524, rfc#1993, Pre-eRFC: Let's fix DSTs, Pre-RFC: Custom DSTs and eRFC: Minimal Custom DSTs via Extern Type (DynSized),
SizeOfValdoes not havesize_of_val/align_of_valmethods to support custom DSTs as this would add to the complexity of this proposal and custom DSTs are not this RFC's focus, see the Custom DSTs section later
Pointee exists at the bottom of the trait hierarchy as a consequence of migrating
away from the ?Sized syntax - enabling the meaning of ?Sized to be re-defined
to be equivalent to SizeOfVal and avoid complicated behaviour change over an
edition.
If an alternative is adopted which keeps the ?Sized syntax then the Pointee
trait is not necessary, such as that described in
Adding ?SizeOfVal.
?Sized is frequently regarded as confusing for new users and came up in the
prior art as a reason why new ?Trait bounds were not seen as desirable
(see rfcs#2255). Furthermore, it isn't clear that ?Sized
scales well to opting out of default bounds that have constness or hierarchies.
This RFC's proposal to migrate from ?Sized is based on ideas from an earlier
pre-eRFC and then a blog post which developed
on those ideas, and the feedback to this RFC's prior art, but is not a load-bearing part
of this RFC.
This RFC proposes removing ?Sized, rewriting it as SizeOfVal, and that
adding any sizedness bound removes the default bound - the "positive bounds"
approach:
| Canonically | Syntax with positive bounds |
|---|---|
Sized |
T: Sized, or T |
SizeOfVal |
T: SizeOfVal (or T: ?Sized on the previous edition) |
Pointee |
T: Pointee |
There are alternatives which can roughly be compared by whether you opt-in or opt-out to the desired sizedness bound, and how implicit or explicit that is:
| Opt-in | Opt-out | |
|---|---|---|
| Implicit | Positive bounds (what is proposed by this RFC) | Adding ?SizeOfVal |
| Explicit | Adding only bounds |
Keeping ?Sized |
Without adding any additional default bounds or relaxed forms, keeping ?Sized could be
compatible with this proposal as follows:
| Canonically | Syntax with positive bounds (as proposed) | Syntax keeping ?Sized (this alternative) |
|---|---|---|
Sized |
T: Sized, or T |
T: Sized or T |
SizeOfVal |
T: SizeOfVal (or T: ?Sized on the previous edition) |
T: ?Sized + SizeOfVal |
Pointee |
T: Pointee |
T: ?Sized or T: ?Sized + Pointee |
In the current edition, ?Sized would need to be equivalent to ?Sized + SizeOfVal to maintain backwards compatibility (see the size_of_val
section for rationale). In the next edition, ?Sized would be
rewritten to ?Sized + SizeOfVal (read: remove the Sized default and add a
SizeOfVal bound) and bare ?Sized would only remove the Sized default
bound.
Another alternative is to make SizeOfVal a default bound in addition to Sized and establish
that relaxing a supertrait bound also implies relaxing subtrait bounds (but that relaxing a
subtrait bound does not imply relaxing supertrait bounds):
| Canonically | Syntax with positive bounds (as proposed) | Syntax adding more relaxed forms (this alternative) |
|---|---|---|
Sized |
T: Sized, or T |
T: Sized or T |
SizeOfVal |
T: SizeOfVal (or T: ?Sized on the previous edition) |
T: ?Sized |
Pointee |
T: Pointee |
T: ?SizeOfVal |
In other words, when a less strict bound is desirable, it is achieved by opting out of the next strictest bound.
Yet another alternative is introducing new syntax that establishes "trait hierarchies", each with their own default bounds as a more explicit concept in the language.
An only keyword is applied to bounds/supertraits when any default bounds from
that trait's hierarchy should be removed.
trait Pointee {}
trait SizeOfVal: only Pointee {}
// ^^^^ adding `only` removes any default bounds/supertraits from
// the hierarchy that the `only`-annotated trait is part of
#[default_trait] // (just for illustration purposes)
trait Sized: only SizeOfVal {}| Bound as written | Interpreted as | Explanation |
|---|---|---|
T |
T: Sized |
Default bound |
T: Sized |
T: Sized + Sized |
Adding an explicit bound alongside the default bound, redundant |
T: only Sized |
T: Sized |
Removing the default bound and adding an explicit bound, redundant |
T: SizeOfVal |
T: SizeOfVal + Sized |
Adding a relaxed bound alongside the default bound, redundant |
T: only SizeOfVal |
T: SizeOfVal |
Removing the default bound and adding a relaxed bound |
T: Pointee |
T: Pointee + Sized |
Adding a relaxed bound alongside the default bound, redundant |
T: only Pointee |
T: Pointee |
Removing the default bound and adding a relaxed bound |
only cannot be used on user-defined traits or trait aliases, even those with
sizedness supertraits. only cannot be used on supertraits in user-defined
trait definitions.
With some of the above alternatives, the ability to incrementally stabilise new
sizedness traits is made more challenging. For example, to express a T: Pointee bound, T: ?SizeOfVal is written, so stability of a degree of
sizedness is based not on the stability of its trait but rather by stability of
being able to relax the next strictest trait.
Pointee is distinct from the existing unstable trait std::ptr::Pointee
from rfcs#2580 as adding a trait with an associated item
to this hierarchy of traits would be backwards incompatible, breaking the example below
as it would be ambiguous whether T::Metadata refers to Pointee::Metadata or
HasAssocType::Metadata and it is unambiguous today.
trait HasAssocType {
type Metadata;
}
fn foo<T: HasAssocType>() -> T::Metadata { // error! ambiguous
todo!()
}This backwards incompatibility also exists when adding methods to any of the
proposed marker traits. For example, assume methods x and y were added to SizeOfVal,
then existing calls to methods with the same names would be broken:
trait HasMethods {
fn x() {}
fn y(&self) {}
}
fn foo<T: HasMethods>(t: &T)
T::x(); // error! ambiguous
t.y(); // error! ambiguous
}Due to Sized being a default bound, the new marker traits being supertraits, and the
edition migration (so that breakages could happen even with ?Sized), this backwards
incompatibility would occur immediately when associated types or methods are added.
Instead of introducing a new marker trait, there are three alternatives that would
enable std::ptr::Pointee to be re-used:
-
Introduce some mechanism to indicate that associated types or methods of a trait could only be referred to with fully-qualified syntax.
-
Introduce a behaviour where the associated types of subtraits take priority over the associated types of supertraits.
Pointeewill effectively become a supertrait of all traits so its associated types would never take precedence. -
Introduce forward-compatibility lints in current edition, the new traits were introduced in the next edition and the edition migration previously described in the next next edition.
-
Reformulate how
std::ptr::Pointeeworks so that it doesn't have a associated type
SizeOfVal is defined as inspecting pointer metadata to compute the size,
which is how the size of all existing non-Sized types is determined. An
alternative to SizeOfVal is ValueSized, which would have a more general
definition of requiring a reference to a value to compute its size.
ValueSized has a broader definition than SizeOfVal which does not match
the current behaviour of ?Sized exactly. ValueSized has a downside that
its interaction with mutexes introduces the opportunity for deadlocks which
are unintuitive:
Consider a version of the CStr type which is a dynamically sized and
computes its size by counting the characters before the null byte (this
is different from the existing std::ffi::CStr which is SizeOfVal).
CStr would implement ValueSized. If this type were used in a Mutex<T>
then the mutex would also implement ValueSized and require locking itself
to compute the size of the CStr that it guards, which could result in
unexpected deadlocks:
let mutex = Mutex::new(CStr::from_str("foo"));
let _guard = mutex.lock().unwrap();
size_of_val(&mutex); // deadlock!SizeOfVal avoids this hazard by keeping the size of dynamically sized
types in pointer metadata, which can be accessed without locking a mutex.
Extern types from rfcs#1861 would remain blocked if no action was taken and this RFC was not accepted, unless:
- The language team decided that having
size_of_valandalign_of_valpanic was acceptable - The language team decided that having
size_of_valandalign_of_valreturn0and1respectively was acceptable - The language team decided that extern types could not be instantiated into generics and that this was acceptable
- The language team decided that having
size_of_valandalign_of_valproduce post-monomorphisation errors for extern types was acceptable
Many of the future possibilities depend on the specifics of this RFC to unblock the features they enable:
- Scalable vectors from rfcs#3838 without this RFC would
remain blocked unless special-cased in the type system
- It is not possible to add these without the
const Sizedfuture possibility
- It is not possible to add these without the
All of the trait names proposed in the RFC can be bikeshed and changed, they'll ultimately need to be decided but aren't the important part of the RFC.
There have been many previous proposals and discussions attempting to resolve
the size_of_val and align_of_val questions for extern types through modifications to
the Sized trait. Many of these proposals include a DynSized trait, of which
this RFC's SizeOfVal trait is inspired.
- rfcs#709: truly unsized types, mzabaluev, Jan 2015
- Earliest attempt to opt-out of
Sized. - Proposes dividing types which do not implement
Sizedinto DSTs and types of indeterminate size.- Adding a field with a
std::marker::NotSizedtype will make a type opt-out ofSized, preventing the type from being used in all the places where it needs to beSized. - Dynamically sized types will "intrinsically" implement
DynamicSize, references to these types will use fat pointers.
- Adding a field with a
- Ultimately postponed for post-1.0.
- Earliest attempt to opt-out of
- rfcs#813: truly unsized types (issue), pnkfelix, Feb 2015
- Tracking issue for postponed rfcs#709.
- Links to an newer version of rfcs#709, still authored by mzabaluev.
- Proposes being able to opt-out of
Sizedwith a negative impl (aCStrtype containing only ac_charis the example given of a DST which would opt-out ofSized).- Also proposes removing
Sizedbound on variousAsPtr/AsMutPtr/FromPtr/FromMutPtrtraits as they existed at the time, so that a user might be able to implement these to preserve the ability to use a thin pointer for their unsized type when that is possible.
- Also proposes removing
- Ultimately closed after rfcs#1861 was merged and intended that rfcs#2255 be used to discuss the complexities of that proposal.
- rfcs#1524: Custom Dynamically Sized Types, strega-nil, Mar 2016
- Successor of rfcs#709/rfcs#813.
- Proposes an
unsafe trait !Sized(which isn't just a negative impl), with an associated typeMetaandsize_of_valmethod.- Under this proposal, users would create a "borrowed" version of their
type (e.g. what
[T]is toVec<T>) which has a zero-sized last field, which is described in the RFC as "the jumping off point for indexing your block of memory". - These types would implement
!Sized, providing a type forMetacontaining any extra information necessary to compute the size of the DST (e.g. a number of strides) and an implementation ofsize_of_valfor the type. - There would be intrinsics to help make create instances of
these dynamically sized types, namely
make_fat_ptr,fat_ptr_metaandsize_of_prelude.
- Under this proposal, users would create a "borrowed" version of their
type (e.g. what
- rfcs#1861: extern types, canndrew, Jan 2017
- Merged in Jul 2017.
- This RFC mentions the issue with
size_of_valandalign_of_valbut suggests that these functions panic in an initial implementation and that "before this is stabilised, there should be some trait bound or similar on them that prevents their use statically". Inventing an exact mechanism was intended to be completed by rfcs#1524 or its like.
- rfcs#1993: Opaque Data structs for FFI, mystor, May 2017
- This RFC was an alternative to the original extern types RFC
(rfcs#1861) and introduced the idea of a
DynSizedauto trait. - Proposes a
DynSizedtrait which was a built-in, unsafe, auto trait, a supertrait ofSized, and a default bound which could be relaxed with?DynSized.- It would automatically be implemented for everything that didn't have an
Opaquetype in it (RFC 1993's equivalent of anextern type). size_of_valandalign_of_valwould have their bounds changed toDynSized.- Trait objects would have a
DynSizedbound by default and theDynSizedtrait would havesize_of_valandalign_of_valmember functions.
- It would automatically be implemented for everything that didn't have an
- Ultimately closed as rfcs#1861 was entering final comment period.
- This RFC was an alternative to the original extern types RFC
(rfcs#1861) and introduced the idea of a
- rust#43467: Tracking issue for RFC 1861, aturon, Jul 2017
- Tracking thread created for the implementation of rfc#1861.
- In 2018, the language team had consensus against having
size_of_valreturn a sentinel value and adding any trait machinery, likeDynSized, didn't seem worth it, preferring to panic or abort.- This was considering
DynSizedwith a relaxed bound. - Anticipating some form of custom DSTs, there was the possibility
that
size_of_valcould run user code and panic anyway, so making it panic for extern types wasn't as big an issue.size_of_valrunning in unsafe code could be a footgun and that caused mild concern. - See this comment and this comment.
- This was considering
- Conversation became more sporadic following 2018 and most
recent discussion was spurred by the
Sized, DynSized and Unsized blog post.
- See this comment onwards.
- It's unclear how different language team opinion is since the 2018 commentary, but posts like above suggest some change.
- rust#44469: Add a
DynSizedtrait, plietar, Sep 2017- This pull request intended to implement the
DynSizedtrait from rfcs#1993. DynSizedas implemented is similar to that from rfcs#1993 except it is implemented for every type with a known size and alignment at runtime, rather than requiring anOpaquetype.- In addition to preventing extern types being used in
size_of_valandalign_of_val, this PR is motivated by wanting to have a mechanism by which!DynSizedtypes can be prevented from being valid in struct tails due to needing to know the alignment of the tail in order to calculate its field offset. DynSizedhad to be made an implicit supertrait of all traits in this implementation - it is presumed this is necessary to avoid unsized types implementing traits.- This actually went through FCP and would have been merged if not eventually closed for inactivity.
- This pull request intended to implement the
- rust#46108: Add DynSized trait (rebase of #44469), mikeyhew, Nov 2017
- This pull request is a resurrection of rust#44469.
- Concerns were raised about the complexity of adding another
?Traitto the language, and suggested that havingsize_of_valpanic was sufficient (the current implementation does not panic and returns zero instead, which is also deemed undesirable).- It was argued that
?Traits are powerful and should be made more ergonomic rather than avoided.
- It was argued that
- kennytm left a useful comment summarising which
standard library bounds would benefit from relaxation to a
DynSizedbound. - Ultimately this was closed after a language team meeting
deciding that
?DynSizedwas ultimately too complex and couldn't be justified by support for a relatively niche feature like extern types.
- rfcs#2255: More implicit bounds (?Sized, ?DynSized, ?Move), kennytm, Dec 2017
- Issue created following rust#46108 to discuss the
complexities surrounding adding new traits which would benefit from relaxed
bounds (
?Traitsyntax). - There have been various attempts to introduce new auto traits with
implicit bounds, such as
DynSized,Move,Leak, etc. Often rejected due to the ergonomic cost of relaxed bounds.?Traitbeing a negative feature can be confusing to users.- Downstream crates need to re-evaluate every API to determine if adding
?Traitmakes sense, for each?Traitadded.- This is also true of the traits added in this proposal, regardless of whether a relaxed bound or positive bound syntax is used. However, this proposal argues that adding supertraits of an existing default bound significantly lessens this disadvantage (and moreso given the niche use cases of these particular supertraits).
- This thread was largely motivated by the
Movetrait and that was replaced by thePintype, but there was an emerging consensus thatDynSizedmay be more feasible due to its relationship withSized.
- Issue created following rust#46108 to discuss the
complexities surrounding adding new traits which would benefit from relaxed
bounds (
- Pre-eRFC: Let's fix DSTs, mikeyhew, Jan 2018
- This eRFC was written as a successor to rfcs#1524.
- It proposes
DynSizedtrait and a bunch of others.DynSizedis a supertrait ofSized(indirectly) and contains asize_of_valmethod. This proposal is the first to removeSizedbounds if another sized trait (e.g.DynSized) has an explicit bound.- This enables deprecation of
?Sizedlike this RFC proposes.
- This enables deprecation of
- A
Thintype to allow thin pointers to DSTs is also proposed in this pre-eRFC - it is a differentThinfrom the currently unstablecore::ptr::Thinand it's out-of-scope for this RFC to include a similar type and accepted rfcs#2580 overlaps. - This pre-eRFC may be the origin of the idea for a family of
Sizedtraits, later cited in Sized, DynSized, and Unsized. - rfcs#2510 was later submitted which was a
subset of this proposal (but none of the
DynSizedparts). - This eRFC ultimately fizzled out and didn't seem to result in a proper RFC being submitted.
- rfcs#2310: DynSized without ?DynSized, kennytm, Jan 2018
- This RFC proposed an alternative version of
DynSizedfrom rfcs#1993/rust#44469 but without being an implicit bound and being able to be a relaxed bound (i.e. no?DynSized). - The proposed
DynSizedtrait in rfcs#2310 is really quite similar to theSizeOfValtrait proposed by this RFC except:- It includes an
#[assume_dyn_sized]attribute to be added toT: ?Sizedbounds instead of replacing them withT: SizeOfVal, which would warn instead of error when a non-SizeOfValtype is substituted intoT.- This is to avoid a backwards compatibility break for uses of
size_of_valandalign_of_valwith extern types, but it is unclear why this is necessary given that extern types are unstable.
- This is to avoid a backwards compatibility break for uses of
- It does not include
Pointee. - Adding an explicit bound for
SizeOfValwould not remove the implicit bound forSized.
- It includes an
- This RFC proposed an alternative version of
- rust#49708:
extern typecannot supportsize_of_valandalign_of_val, joshtriplett, Apr 2018- Primary issue for the
size_of_val/align_of_valextern types blocker, following no resolution from either of rfcs#1524 and rust#44469 or their successors. - This issue largely just re-hashes the arguments made in other threads summarised here.
- Primary issue for the
- Pre-RFC: Custom DSTs, ubsan, Nov 2018
- This eRFC was written as a successor to rfcs#1524.
- Proposes addition of a
DynamicallySizedtrait with aMetadataassociated type andsize_of_valandalign_of_valmember functions.- It has an automatic implementation for all
Sizedtypes, whereMetadata = ()andsize_of_valandalign_of_valjust callsize_ofandalign_of. - It can be manually implemented for DSTs and if it is, the type will
not implement
Sized.
- It has an automatic implementation for all
- Due to
DynamicallySizednot being a supertrait ofSized, this proposal had no way of modifying the bounds ofsize_of_valandalign_of_valwithout it being a breaking change (and so did not propose doing so). - This eRFC ultimately fizzled out and didn't seem to result in a proper RFC being submitted.
- rfcs#2594: Custom DSTs, strega-nil, Nov 2018
- This eRFC was written as a successor to rfcs#1524.
- This is more clearly a direct evolution of rfcs#1524 than other successors were, unsurprisingly given the same author.
- Proposes a
Pointeetrait withMetadataassociated type and aContiguoussupertrait ofPointeewithsize_of_valandalign_of_valmembers.Sizedis a subtrait ofPointee<Metadata = ()>(as sized types have thin pointers).Sizedalso implementsContiguouscallingsize_ofandalign_offor each of the member functions.- Dynamically sized types can implement
Pointeemanually and provide aMetadataassociated type, and thenContiguousto implementsize_of_valandalign_of_val. - Intrinsics are added for constructing a pointer to a dynamically sized type from its metadata and value, and for accessing the metadata of a dynamically sized type.
- extern types do not implement
Contiguousbut do implementPointee. Contiguousis a default bound and so has a relaxed form?Contiguous.
- There's plenty of overlap here with rfcs#2580
and its
Pointeetrait - the accepted rfcs#2580 does not makeSizeda subtrait ofPointeeor have aContiguoustrait but thePointeetrait is more or less compatible. - Discussed in a November 4th 2020 design meeting
(pre-meeting notes and
post-meeting notes).
- Meeting was mostly around rfcs#2580 but mentioned the state of Custom DSTs.
- Mentioned briefly in a language team triage meeting in March 2021 and postponed until rfcs#2510 was implemented.
- This eRFC was written as a successor to rfcs#1524.
- Design Meeting, Language Team, Jan 2020
- Custom DSTs and
DynSizedare mentioned but there aren't any implications for this RFC.
- Custom DSTs and
- rfcs#2984: introduce
PointeeandDynSized, nox, Sep 2020- This RFC aims to land some traits in isolation so as to enable progress on other RFCs.
- Proposes a
Pointeetrait with associated typeMeta(very similar to accepted rfcs#2580) and aDynSizedtrait which is a supertrait of it.Sizedis made a supertrait ofDynSized<Meta = ()>. Neither new trait can be implemented by hand.- It's implied that
DynSizedis implemented for all dynamically sized types, but it isn't clear.
- It's implied that
- Despite being relatively brief, RFC 2984 has lots of comments.
- The author argues that
?DynSizedis okay and disagrees with previous concerns about complexity and that all existing bounds would need to be reconsidered in light of?DynSized.- In response, it is repeatedly argued that there is a mild
preference for making
size_of_valandalign_of_valpanic instead of adding?Traitbounds and that having the ability to doPointee<Meta = ()>type bounds is sufficient.
- In response, it is repeatedly argued that there is a mild
preference for making
- The author argues that
- Exotically sized types (
DynSizedandextern type), Language Team, Jun 2022- Despite being published in Jun 2022, these are reportedly notes from a previous Jan 2020 meeting, but not the one above.
- Explores constraints
Arc/RcandMuteximply onDynSizedbounds. SizeOfValis first mentioned in these meeting notes, as when the size/ alignment can be known from pointer metadata.
- eRFC: Minimal Custom DSTs via Extern Type (DynSized), CAD97, May 2022
- This RFC proposes a forever-unstable default-bound unsafe trait
DynSizedwithsize_of_val_rawandalign_of_val_raw, implemented for everything other than extern types. Users can implementDynSizedfor their own types. This proposal doesn't say whetherDynSizedis a default bound but does mention a relaxed form of the trait?DynSized.
- This RFC proposes a forever-unstable default-bound unsafe trait
- rfcs#3319: Aligned, Jules-Bertholet, Sep 2022
- This RFC aims to separate the alignment of a type from the size of the
type with an
Alignedtrait.- Automatically implemented for all types with an alignment (includes
all
Sizedtypes). Alignedis a supertrait ofSized.
- Automatically implemented for all types with an alignment (includes
all
- This RFC aims to separate the alignment of a type from the size of the
type with an
- rfcs#3396: Extern types v2, Skepfyr, Feb 2023
- Proposes a
SizeOfValtrait for types whose size and alignment can be determined solely from pointer metadata without having to dereference the pointer or inspect the pointer's address.- Under this proposal,
[T]isSizeOfValas the pointer metadata knows the size, rather thanDynSized. - Basically identical to this RFC's
SizeOfVal.
- Under this proposal,
- Attempts to sidestep backwards compatibility issues with introducing a
default bound via changing what
?Sizedmeans across an edition boundary. - Discussed in a language team design meeting.
- Proposes a
- rfcs#3536: Trait for
!Sizedthin pointers, jmillikin, Nov 2023- Introduces unsafe trait
DynSizedwith asize_of_valmethod.- It can be implemented on
!Sizedtypes.- It is an error to implement it on
Sizedtypes.
- It is an error to implement it on
- References to types that implement
DynSizeddo not need to store the size in pointer metadata. Types implementingDynSizedwithout other pointer metadata are thin pointers.
- It can be implemented on
- This proposal has no solution for extern type limitations, its sole aim is to enable more pointers to be thin pointers.
- Introduces unsafe trait
- Sized, DynSized, and Unsized, Niko Matsakis, Apr 2024
- This proposes a hierarchy of
Sized,DynSizedandUnsizedtraits like in this RFC and proposes deprecatingT: ?Sizedin place ofT: Unsizedand sometimesT: DynSized. Adding a bound for any ofDynSizedorUnsizedremoves the defaultSizedbound.DynSizedis very similar to this RFC'sSizeOfValUnsizedis the same as this RFC'sPointee
- As described below it is the closest inspiration for this RFC.
- This proposes a hierarchy of
There are some even older RFCs that have tangential relevance that are listed below but not summarized:
- rfcs#5: virtual structs, nrc, Mar 2014
- rfcs#9: RFC for "fat objects" for DSTs, MicahChalmer, Mar 2014
- pre-RFC: unsized types, japaric, Mar 2016
There haven't been any particular proposals which have included a solution for runtime-sized types, as the scalable vector types proposal in RFC 3838 is relatively newer and less well known:
- rfcs#3268: Add scalable representation to allow support for scalable vectors, JamieCunliffe, May 2022
- Proposes temporarily special-casing scalable vector types to be able to
implement
Copywithout implementingSizedand allows function return values to beCopyorSized(not justSized).
- Proposes temporarily special-casing scalable vector types to be able to
implement
- rfcs#3838:
rustc_scalable_vector, davidtwco, Jul 2025- Revised version of rfcs#3268 which depends on the
const SizedFuture Possibility to enable scalable vector types to beSizedand thus implementCopy, be used as local variables and as return values
- Revised version of rfcs#3268 which depends on the
To summarise the above exhaustive listing of prior art:
- One proposal proposed adding a marker type that as a field would result in the
containing type no longer implementing
Sized. - Often proposals focused at Custom DSTs preferred to combine the
escape-the-sized-hierarchy part with the Custom DST machinery.
- e.g.
DynSizedtrait withMetadataassociated types andsize_of_valandalign_of_valmethods, or a!Sizedpseudo-trait that you could implement. - Given the acceptance of rfcs#2580, Rust
doesn't seem to be trending in this direction, as the
Metadatapart of this is now part of a separatePointeetrait.
- e.g.
- Most early
DynSizedtrait proposals (independent or as part of Custom DSTs) would makeDynSizeda default bound mirroringSized, and consequently had a relaxed form?DynSized.- Later proposals were more aware of the language team's resistance towards adding new relaxed bounds and tried to avoid this.
- Backwards compatibility concerns were the overriding reason for the rejection
of previous
DynSizedproposals.- These can be sidestepped by relying on being a supertrait of
Sized.
- These can be sidestepped by relying on being a supertrait of
The Rationale and Alternatives section provides rationale for some of the decisions made in this RFC and references the prior art above when those proposals made different decisions.
No previous proposal captures the specific part of the design space that this proposal attempts to, but these proposals are the closest matches for parts of this proposal:
- Pre-eRFC: Let's fix DSTs was the only other proposal
removing
Sizedbounds when a bound for another sized trait (onlyDynSizedin that pre-eRFC's case) was present.- However, this proposal had
size_of_valmethods in itsDynSizedtrait and proposed a bunch of other things necessary for Custom DSTs.
- However, this proposal had
- rfcs#2310: DynSized without ?DynSized was
proposed at a similar time and was similarly focused only on making
Sizedmore flexible, but had a bunch of machinery for avoiding backwards incompatibility that this RFC believes is unnecessary. Like this proposal, it avoided makingDynSizeda default bound and avoided having a relaxed form of it.- However, this proposal didn't suggest removing default
Sizedbounds in the presence of other size trait bounds.
- However, this proposal didn't suggest removing default
- rfcs#3396: Extern types v2 identified that
SizeOfValspecifically was necessary moreso thanDynSizedorValueSizedand serves as the inspiration for this RFC'sSizeOfVal. - Sized, DynSized, and Unsized is very similar and a major inspiration for this proposal. It has everything this proposal has except for the future possibilities and all the additional context an RFC needs.
Some prior art referenced rust#21974 as a limitation of the type system which can result in new implicit bounds or implicit supertraits being infeasible for implementation reasons, but it is believed that this is no longer relevant.
-
What names should be used for the traits?
-
Which syntax should be used for opting out of a default bound and a trait hierarchy? (and in the future, opting out of a default bound with const traits and a trait hierarchy)
- This RFC is primarily written proposing the "positive bounds" approach, where introducing a positive bound for a supertrait of the default bound will remove the default bound
- Alternatively, described in Adding
?SizeOfVal, existing relaxed bounds syntax could be used, where a desired bound is written as opting out of the next strictest - In a February 2025 design meeting with the language team, a strong bias towards the positive bounds alternative was expressed, arguing that while a explicit sigil indicating an opt-out is happening is valuable, both alternatives have unintuitive aspects, but that "asking for what you want" (as in the positive bounds alternative) is less confusing than "asking for the next strictest thing you don't need" (as in the relaxed bounds alternative)
- There has since been interest from the language team in the
onlybounds alternative
-
Should
std::ptr::Pointeebe re-used instead of introducing a new marker trait?- In a February 2025 design meeting with the language team, no strong opinion was
expressed on this question. There are open proposals to change
std::ptr::Pointeeto no longer have an associated type, which would render this unresolved question moot. A mild preference for the second alternative described in Why not re-usestr::ptr::Pointee? was also shared
- In a February 2025 design meeting with the language team, no strong opinion was
expressed on this question. There are open proposals to change
-
As described in a the
const Sizedfuture possibility, this could be extended to supporting scalable vector types in addition to extern types, by introducing aconst Sizedhierarchy
-
Additional size traits could be added as supertraits of
Sizedif there are other delineations in sized-ness that make sense to be drawn (subject to avoiding backwards-incompatibilities when changing APIs). -
The requirement that users cannot implement any of these traits could be relaxed in future if required.
-
Depending on a trait which has one of the proposed traits as a supertrait could imply a bound of the proposed trait, enabling the removal of boilerplate.
- However, this would limit the ability to relax a supertrait, e.g. if
trait Clone: SizedandT: Cloneis used as a bound of a function andSizedis relied on in that function, then the supertrait ofClonecould no longer be relaxed as it can today. - See
onlybounds for a similar idea
- However, this would limit the ability to relax a supertrait, e.g. if
-
All existing associated types will have at least a
SizeOfValbound and relaxing these bounds is a semver-breaking change. It could be worth considering introducing mechanisms to make this relaxation non-breaking and apply that automatically over an edition- i.e.
type Output: if_rust_2021(Sized) + NewAutoTraitor something like that, out of scope for this RFC
- i.e.
-
Consider allowing traits to relax their bounds and having their implementor have stricter bounds - this would enable traits and implementations to migrate towards more relaxed bounds
- This would be unintuitive to callers but would not break existing code
The following proposals and ideas for evolving Rust build upon or are related to this RFC's ideas:
rfcs#3729: Sized Hierarchy (this RFC)
│
│──→ `const Sized` Hierarchy ──→ Scalable Vectors (rfcs#3838)
│
│──→ Custom DSTs
│
│──→ Alignment traits/`DataSizeOf`/`DataAlignOf` (size != stride)
│
└──→ wasm `externref` types
Rust already supports SIMD (Single Instruction Multiple Data), which allows operating on multiple values in a single instruction. Processors have SIMD registers of a known, fixed length and a variety of intrinsics which operate on these registers. For example, x86-64 introduced 128-bit SIMD registers with SSE, 256-bit SIMD registers with AVX, and 512-bit SIMD registers with AVX-512, and Arm introduced 128-bit SIMD registers with Neon.
As an alternative to releasing SIMD extensions with greater bit widths, Arm and RISC-V have vector extensions (SVE/Scalable Vector Extension and the "V" Vector Extension/RVV respectively) where the bit width of vector registers depends on the CPU implementation, and the instructions which operate these registers are bit width-agnostic.
As a consequence, these types are not Sized in the Rust sense, as the size of
a scalable vector cannot be known during compilation, but is a runtime constant.
For example, the size of these types could be determined by inspecting the value
in a register - this is not available at compilation time and the value may
differ between any given CPU implementation. Both SVE and RVV have mechanisms to
change the system's vector length (up to the maximum supported by the CPU
implementations) but this is not supported by the proposed ABI for these types.
However, despite not implementing Sized, these are value types which should
implement Copy and can be returned from functions, can be variables on the
stack, etc. These types should implement Copy but given that Sized is a
supertrait of Copy, they cannot be Copy without being Sized, and they
aren't Sized.
Furthermore, these types can be used with size_of and
size_of_val, but should not be usable in a const context.
Like extern types, scalable vectors require an extension of the Sized
trait - being able to distinguish between types that do implement Sized, but only
at runtime or at both runtime and compile-time.
A sketch of that is presented here, but would either become its own dedicated RFC, or be integrated in rfcs#3838: Scalable Vectors. It is written assuming rfcs#3762: const traits, it is not strictly necessary, but avoids complex alternatives that would require more marker traits to model the runtime-sized types.
In this future possibility, Sized and SizeOfVal become const traits. Types
automatically have const implementations when the type has a size known at
compilation time. Sized is const if-and-only-if SizeOfVal is const:
┌────────────────┐ ┌─────────────────────────────┐
│ const Sized │ ───────────────────────→ │ Sized │
│ {type, target} │ implies │ {type, target, runtime env} │
└────────────────┘ └─────────────────────────────┘
│ │
implies implies
│ │
↓ ↓
┌──────────────────────────────┐ ┌───────────────────────────────────────────┐
│ const SizeOfVal │ ──────────→ │ SizeOfVal │
│ {type, target, ptr metadata} │ implies │ {type, target, ptr metadata, runtime env} │
└──────────────────────────────┘ └───────────────────────────────────────────┘
│
implies
│
┌───────────────────────────┘
↓
┌──────────────────┐
│ Pointee │
│ {runtime env, *} │
└──────────────────┘
Or, in Rust syntax:
#![feature(const_trait_impl)]
#[const_trait] trait Sized: ~const SizeOfVal {}
#[const_trait] trait SizeOfVal: Pointee {}
trait Pointee {}Note
For an accessible summary with more details, the author of this RFC has given
a talk at Rust Nation 2026
about this proposal including the const Sized future possibility.
When const sizedness is introduced, all existing types are const Sized or
const SizeOfVal, and only scalable vectors are non-const Sized. rustc can
require non-const Sized for local variables and types of return values, and
Clone can require a non-const implementation of Sized in its
supertrait, permitting Clone and Copy to be implemented by
scalable vectors.
size_of is modified to accept a T: ~const Sized, so that
size_of is a const function if-and-only-if Sized has a const
implementation:
pub const fn size_of<T: ~const Sized>() -> usize {
/* .. */
}This has the potential to break existing code like uses_size_of in the below
example. However, per Before the default trait, const Sized would become the default bound and require a migration so the below
examples would not break:
fn uses_size_of<T: Sized>() -> usize {
const { std::mem::size_of<T>() }
}
fn another_use_of_size_of<T: Sized>() -> [u8; size_of::<T>()] {
std::array::repeat(0)
}Similarly, size_of_val is modified to accept a T: ~const SizeOfVal:
pub const fn size_of_val<T: ~const SizeOfVal>() -> usize {
/* .. */
}While it is theoretically possible for size_of and size_of_val to accept
runtime-sized types in a const context and use the runtime environment of the host
when computing the size of the types, this is not recommended7.
const Sized and const SizeOfVal bounds are compatible with the proposed
"positive bounds" syntax for the Sized hierarchy, as well as the alternatives
presented in Why remove default bounds when a sizedness bound is
present?:
| Canonically | Syntax with positive bounds (as proposed) | Syntax keeping ?Sized (first alternative) |
Syntax adding more relaxed forms (second alternative) |
|---|---|---|---|
const Sized |
T: const Sized, or T |
T: const Sized or T |
T: const Sized or T |
Sized |
T: Sized on the next edition, N/A on previous edition |
T: ?(const Sized) + Sized |
T: ?(const Sized) |
const SizeOfVal |
T: const SizeOfVal (or T: ?Sized on the previous edition) |
T: ?(const Sized) + const SizeOfVal (or T: ?Sized on the previous edition) |
T: ?Sized |
SizeOfVal |
T: SizeOfVal |
T: ?(const Sized) + SizeOfVal |
T: ?(const SizeOfVal) |
Pointee |
T: Pointee |
T: ?(const Sized) or T: ?(const Sized) + Pointee |
T: ?SizeOfVal |
Or with the third alternative, only bounds:
| Bound as written | Interpreted as | Explanation |
|---|---|---|
T |
T: const Sized |
Default bound |
T: const Sized |
T: const Sized |
Adding an explicit bound alongside the default bound, redundant |
T: only const Sized |
T: const Sized |
Removing the default bound and adding an explicit bound, redundant |
T: Sized |
T: const Sized |
Adding an explicit bound alongside the default bound, redundant |
T: only Sized |
T: Sized |
Removing the default bound and adding an explicit bound |
T: const SizeOfVal |
T: const Sized |
Adding a relaxed bound alongside the default bound, redundant |
T: only const SizeOfVal |
T: const SizeOfVal |
Removing the default bound and adding a relaxed bound |
T: SizeOfVal |
T: const Sized |
Adding a relaxed bound alongside the default bound, redundant |
T: only SizeOfVal |
T: SizeOfVal |
Removing the default bound and adding a relaxed bound |
T: Pointee |
T: const Sized |
Adding a relaxed bound alongside the default bound, redundant |
T: only Pointee |
T: Pointee |
Removing the default bound and adding a relaxed bound |
const Sized is concrete example of the Before the default
trait future compatibility and the migration strategy
described in that section would be necessary to avoid a const Sized default
bound.
Despite the introduction of const Sized, there is still one niche backwards
incompatibility that would be necessary to support scalable vectors implementing
Clone and thus Copy.
const fn f<T: Clone + ?Sized>() {
let _ = size_of::<T>();
}
// or..
fn f<T: Clone + ?Sized>() {
let _ = const { size_of::<T>() };
}In the above example, f opts-out of the default Sized bound but a Sized
bound is implied by its Clone bound. Clone's Sized supertraits will not
migrated to const Sized in the proposed migration, which would result in the
above example no longer compiling.
This is a niche case - it relies on code explicitly opting out of a Sized
bound, but having a Sized implied by Clone, and then using that
parameter somewhere with a const Sized requirement. It is hoped that
this is sufficiently rare that it does not block this proposal, and that
any such cases in the open source ecosystem could be identified with a crater
run and addressed by a patch.
It could be easily fixed by removing the unnecessary ?Sized relaxation
and using the implicit T: const Sized bound, or by adding an explicit
T: const Sized bound.
Another compelling feature that requires extensions to Rust's sizedness traits to
fully support is Wasm's externref. externref types are opaque types that cannot
be put in memory 8. externrefs are used as abstract handles to resources in the
host environment of the Wasm program, such as a JavaScript object. Similarly, when
targetting some GPU IRs (such as SPIR-V), there are types which are opaque handles
to resources (such as textures) and these types, like Wasm's externref, cannot
be put in memory.
externref are similar to Pointee in that the type's size is not known, but unlike
Pointee cannot be used behind a pointer. This RFC's proposed hierarchy of traits could
support this by adding another supertrait, Value:
┌────────────────┐
│ Sized │
│ {type, target} │
└────────────────┘
│
implies
│
↓
┌──────────────────────────────┐
│ SizeOfVal │
│ {type, target, ptr metadata} │
└──────────────────────────────┘
│
implies
│
↓
┌─────────┐
│ Pointee │
│ {*} │
└─────────┘
│
implies
│
↓
┌───────┐
│ Value │
│ {*} │
└───────┘
Pointee is still defined as being implemented for any type that can be used
behind a pointer and may not be sized at all, this would be implemented for
effectively every type except Wasm's externref (or similar opaque types from
some GPU targets). Value is defined as being implemented for any type that can
be used as a value, which is all types, and also may not be sized at all.
Earlier in this RFC, extern types have previously been described as not being
able to be used as a value, but it could instead be permitted to write functions
which use extern types as values (e.g. such as taking an extern type as an argument),
and instead rely on it being impossible to get a extern type that is not behind a
pointer or a reference. This also implies that SizeOfVal types can be used as values,
which would remain prohibited behind the unsized_locals and unsized_fn_params
features until these are stabilised.
With these changes to the RFC and possibility additional changes to the language, it
could be possible to support Wasm's externref and opaque types from some GPU targets.
There has been community interest in an Aligned trait and there
are examples of Aligned traits being added in the ecosystem:
rustchas its ownAlignedtrait to support pointer tagging.unsized-vecimplements aVecthat depends on knowing whether a type has an alignment or not.
An Aligned trait hierarchy could be introduced alongside this proposal. It wouldn't be
viable to introduce Aligned within this hierarchy, as dyn Trait which is SizeOfVal
would not be aligned, but some extern types could be Aligned, so there isn't an obvious
place that an Aligned trait could be included in this hierarchy.
The hierarchy proposed in this RFC could easily be extended per Future
compatibility and migration to support types
whose size differ from its stride. For example, as @tmandry
described (building on the const Sized future
possibility):
// `size_of`
const trait SizeOf: ~const SizeOfVal {}
/// `size_of_val`
const trait SizeOfVal: ~const DataSizeOfVal {}
/// `data_size_of` + `align_of` + optionally `stride_of`
const trait DataSizeOf: ~const DataSizeOfVal {}
/// `data_size_of_val` + `align_of_val` + optionally `stride_of_val`
const trait DataSizeOfVal: Pointee {}
/// `&T`
trait Pointee {}Given the community interest in supporting custom DSTs in the future (see prior art), this RFC was written considering future-compatibility with custom DSTs in mind.
There are various future changes to these traits which could be used to support custom DSTs on top of this RFC. None of these have been considered thoroughly, and are written here only to illustrate.
- Allow
std::ptr::Pointeeto be implemented manually on user types, which would replace the compiler's implementation. - Introduce a trait like rfcs#2594's
Contiguouswhich users can implement on their custom DSTs, or add methods toSizeOfValand allow it to be implemented by users. - Introduce intrinsics which enable creation of pointers with metadata and for accessing the metadata of a pointer.
SizeOfValComputed could be introduced as a complement to SizeOfVal, if there
are types whose size cannot be stored in pointer metadata (or where this is not
desirable):
┌────────────────┐
│ Sized │
│ {type, target} │
└────────────────┘
│
implies
│
↓
┌──────────────────────────────┐
│ SizeOfVal │
│ {type, target, ptr metadata} │
└──────────────────────────────┘
│
implies
│
↓
┌─────────────────────────────────────┐
│ SizeOfValComputed │
│ {type, target, ptr metadata, value} │
└─────────────────────────────────────┘
│
implies
│
↓
┌─────────┐
│ Pointee │
│ {*} │
└─────────┘
Footnotes
-
Dynamic stack allocation does exist, such as in C's Variable Length Arrays (VLA), but not in Rust (without incomplete features like
unsized_localsandunsized_fn_params). ↩ -
Adding a new automatically implemented trait and adding it as a bound to an existing function is backwards-incompatible with generic functions. Even though all types could implement the trait, existing generic functions will be missing the bound.
If
Foowere introduced to the standard library and implemented on every type, and it was added as a bound tosize_of(or any other generic parameter)..auto trait Foo {} fn size_of<T: Sized + Foo>() { /* .. */ } // `Foo` bound is new!..then user code would break:
↩fn do_stuff<T>(value: T) { size_of(value) } // error! the trait bound `T: Foo` is not satisfied -
Trait objects passed by callers would not imply the new trait.
If
Foowere introduced to the standard library and implemented on every type, and it was added as a bound tosize_of_val(or any other generic parameter)..auto trait Foo {} fn size_of_val<T: ?Sized + Foo>(x: val) { /* .. */ } // `Foo` bound is new!..then user code would break:
↩fn do_stuff(value: Box<dyn Display>) { size_of_val(value) } // error! the trait bound `dyn Display: Foo` is not satisfied in `Box<dyn Display>` -
Callers of existing APIs will have one of the following
Sizedbounds:Before ed. migration After ed. migration T: Sized(implicit or explicit)T: Sized(implicit or explicit)T: ?SizedT: SizeOfValAny existing free function in the standard library with a
T: SizedorT: ?Sizedbound could be changed to one of the following bounds and remain compatible with any callers that currently exist (as per the above table):
↩SizedSizeOfValPointeeSized✔ (no change) ✔ ✔ SizeOfValBackwards incompatible ✔ (no change) ✔ -
In a crate defining a trait which has method with sizedness bounds, such as..
trait Foo { fn bar<T: Sized>(t: T) -> usize; fn baz<T: ?Sized>(t: T) -> usize; }..then an implementor of
Foomay rely on the existing bound and these implementors ofFoowould break if the bounds ofbarorbazwere relaxed.
↩struct Qux; impl Foo for Qux { fn bar<T: Sized>(_: T) -> usize { std::mem::size_of<T> } fn baz<T: ?Sized>(t: T) -> usize { std::mem::size_of_val(t) } } -
Associated types of traits have default
Sizedbounds which cannot be relaxed. For example, relaxing aSizedbound onAdd::Outputbreaks a function which takes aT: Addand passes<T as Add>::Outputtosize_ofas not all types which implement the relaxed bound will implementSized.If a default
Sizedbound on an associated trait, such asAdd::Output, were relaxed in the standard library...trait Add<Rhs = Self> { type Output: SizeOfVal; }...then user code would break:
fn do_stuff<T: Add>() -> usize { std::mem::size_of::<<T as Add>::Output>() } //~^ error! the trait bound `<T as Add>::Output: Sized` is not satisfiedRelaxing the bounds of an associated type is in effect giving existing parameters a less restrictive bound which is not backwards compatible. ↩
-
Despite having some advantages: if implementable within the compiler's interpreter, it could enable accelerated execution of const code - there are multiple downsides to allowing this:
Scalable vectors are platform specific and could require optional target features, which would necessitate use of
cfgs andtarget_featurewith const functions, adding a lot of complexity to const code.More importantly, the size of a scalable vector could differ between the host and the target and if the size from a const context were to be used at runtime with scalable vector types, then that could result in incorrect code. Not only is it unintuitive that
const { size_of::<svint8_t>() }would not be equal tosize_of::<svint8_t>(), but the layout of types could differ between const and runtime contexts which would be unsound.Changing
size_ofandsize_of_valto~const Sizedbounds ensures thatconst { size_of:<svint8_t>() }is not possible. ↩ -
When Rust is compiled to Wasm, we can think of the memory of the Rust program as being backed by something like a
[u8],externrefs exist outside of that[u8]and there is no way to put anexternrefinto this memory, so it is impossible to have a reference or pointer to aexternref.wasm-bindgencurrently supportsexternrefby creating a array of the items which would be referenced by anexternrefon the host side and passes indices into this array across the Wasm-host boundary in lieu ofexternrefs. It isn't possible to support opaque types from some GPU targets using this technique. ↩