---
title: "Design meeting 2025-02-05: Hierarchy of Sized traits part 2"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2025-02-05
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-02-05
url: https://hackmd.io/VeTUAQpxRsqYcZWTjocI-Q
---
We opened an RFC with what we discussed in our last design meeting on the topic and there has been lots of discussion on that. There have been many changes but most of them are small clarifications, there haven't been any major rewrites or very substantive changes in approach as there were with earlier drafts prior to our last design meeting.
There's a summary comment on the RFC here describing all of the changes (and including the previous summary comment and its changes):
https://github.com/rust-lang/rfcs/pull/3729#issuecomment-2517204789
They're split into big and small changes so you can skip the trivial ones. If you remember the RFC well and just want to catch up on the changes, then this link has a diff since our last meeting:
<https://github.com/davidtwco/rfcs/compare/26f0aceabedf5bf17cd11ff26c829438e1659a5d...davidtwco:rfcs:sized-hierarchy?short_path=ff4bf3d#diff-ff4bf3ddea5d93dd09c3415aed6948be8be59f0a14127e03c654cde4aa1d91c2>
Of course, feel free to re-read the whole document if you prefer.
Here's what I'd like to get out of the meeting:
- In our previous meeting, there appeared to be some consensus that this was a problem worth solving and that the solution proposed here was on the right track, I'd like to check that's still the case.
- We've added a couple unresolved questions to the RFC. If any of these questions are ones that the team can quickly reach consensus on then I can update the RFC to incorporate that change. If any of those questions are more contentious then that's okay and we can leave it till we have more experience with the proposal.
- I'd be interested in knowing what next steps should be:
- If there are changes to be made or if the proposal is taking the wrong approach, then what changes should I be making to get closer to something the team would be happy with?
- If the team is happy with the proposal, could we proceed with an experimental implementation*? Would accepting this RFC be blocked on the const traits RFC being accepted or could it be accepted subject to whatever solution for const traits ends up being?
*subject to the people working on const traits being happy with me building atop it.
As a point of information, I've started working on an implementation of this - it currently implements everything up to making the traits const and passes all the tests/builds stage two/etc. So I'm confident that an implementation is feasible and I haven't run into any unexpected backwards incompatibilities (and have had to perform many appropriate and backwards compatible relaxations of bounds in the standard library in that implementation, because extern types are used in std and in rustc today).
Without further ado, the RFC:
<https://github.com/davidtwco/rfcs/blob/0f9ec849ab44c73bd07b16768f7580568134aade/text/3729-sized-hierarchy.md>
---
# Discussion
## Attendance
- People: TC, scottmcm, davidtwco, yosh, nikomatsakis, Urgau, tmandry
## Meeting roles
- Minutes, driver: TC
## Backwards compatibility
nikomatsakis: Today this compiles too...
```rust
const fn f<T: Clone + ?Sized>() {
let _ = size_of::<T>();
}
```
As does:
```rust
pub const fn f<T: Clone + ?Sized>() {
let _ = const { size_of::<T>() };
}
```
...so it seems as if the `Clone` trait would have to be changed to have `const Sized` as a supertrait bound, right? Isn't that a problem?
davidtwco: This is a tricky case that I hadn't thought of.
nikomatsakis: I believe we can perhaps finesse this with an edition transition and/or just investigate if it's an issue in practice; it needs to have the `?Sized` for example to be an issue.
scottmcm: This example is not in a const position, so it only needs `~const Sized`, right?
## Hot takes on the unresolved questions
> Which syntax should be used for 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 ?MetaSized, existing relaxed bounds syntax could be used, where a desired bound is written as opting out of the next strictest.
nikomatsakis: I feel strongly we should use the positive bound approach. I am like 99.9% fixed on this point.
davidtwco: I agree, but the most contentious discussion on the RFC was on this point - some feel the `?` sigil indicating that an opt-out is happening is quite important. My local experimentation uses the alternative syntax just because it was slightly easier to implement but either should be possible.
tmandry: Looking at the table, I agree with the positive bound approach.
scottmcm: I agree with the phrasing of 'I don't want to think of the "next most precise thing"'.
nikomatsakis: What would *persuade* me is to either (a) try the version I want in practice and be unhappy or (b) try it with some people and see them fail miserably.
davidtwco: I've implemented enough of this to know it would be relatively straightforward to implement both syntaxes if we wanted to do this experiment. Implementing the `?Sized` variant also introduces the possibility of incoherent bounds, e.g. `?MetaSized + Sized`, that would need to be an error. With positive bounds, incoherent bounds aren't possible, you just always get the strictest one you wrote.
TC: Maybe we could essentially do both. Perhaps we could support trait aliases like:
```rust!
trait Foo = ?Sized;
```
TC: Niko's argument comes at it from a perspective focused on user experience. RalfJ is coming at this from a POV about language principleness. Maybe the principled thing to do is add the `?` thing and to provide the good user experience through trait aliases. Then people could click through in `rustdoc` to see what's actually going on.
davidtwco: the language supports the above actually (well, `trait X = Y`, `T: ?X`), I use it to introduce this to the standard library without doing `cfg(bootstrap)` everywhere in my experimental implementation
nikomatskais: I think Ralf is actually wrong on this and it's connected to the forwards compatibility issue. Under this meaning, everything desugars to traditional bounds, it's just that there is one you didn't write because it was automatic. But with `?`, you are getting "one level below" in the hierarchy, and that is actually a malleable thing (which is why `?Sized` doesn't work out). Imagine you have three levels of traits:
```rust
trait Level3: Level2 {}
trait Level2: Level1 {}
trait Level1 {}
fn foo<T: ?Level2>() {
// This means... what exactly? It hurts my head to think about it.
// I think it means `T: Level1`?
// davidtwco: it does (thanks!--niko)
}
```
And now you want to add `Level 1.5`, what happens? In particular, `foo::<L1>` used to work, but now .. it doesn't?
But if I had written...
```rust
fn foo<T: Level1>() {}
// this function's meaning remains unchanged.
```
davidtwco: **action item** - express a bias towards positive bounds but that it will be considered later, not a blocker
> Should std::ptr::Pointee be re-used instead of introducing a new marker trait?
>
> This would require an additional changes to avoid ambiguity, as described in Why not re-use str::ptr::Pointee?.
nikomatsakis: I don't have a super strong opinion. I don't actually like the name `Pointee` very much, but I think we've doubled down on it with `CoercePointee`, right? Sad. In retrospect I wish I had suggested `CoercePointerTarget`. Anyway, DISAGREE AND COMMIT. It's fine.
Reading the unresolved questions, it's all about ambiguity, but I think that's a bit odd. We have some RFCs that have been flying around for how to resolve ambiguous method calls involved sub/super trait hierarchies, right? I think I would expect associated types to work the same. My memory is that we've settled on the rule of "prefer the item from the subtrait", under the assumption that the subtrait would not have intentionally shadowed the supertrait, so it presumably pre-exists (which applies here too).
davidtwco: I'm not sure it's about super/sub traits, because the associated item would likely be from a entirely distinct hierarchy.
nikomatsakis: Pointee is the root of all hierarchies, it seems to me, given that `MetaSized` is an implicit supertrait.
davidtwco: ~~is it? `T: Sized: MetaSized: Pointee` and `T: Foo` are distinct? `Foo::Metadata` and `Pointee::Metadata` overlap for `T::Pointee`, and aren't in a hierarchy~~
> What is the precedence for ?const Trait - (?const) Trait or ?(const Trait)? This isn't a question for this RFC to resolve but this RFC takes a conserative approach and always adds explicit parentheses.
nikomatsakis: What the heck is `?const`. I do not want to see this syntax in Rust. Not really any syntax in which `?` appears in front of an identifier.
davidtwco: Discussion on the RFC raised the possible ambiguity, does `?const Trait` opt-out of the const or the whole bound is the key question.
> (added by nikomatsakis) `MetaSized`
nikomatsakis: I do not like `MetaSized` and prefer `SizedVal` or `ValueSized`.
davidtwco: **action item** - add unresolved question for naming of the traits
## Incremental rollout
tmandry: What are the implications of stabilizing a little bit of the hierarchy at a time? Does it need to be done in a particular order?
davidtwco: I don't think there are any complications with this - you can stabilise the new traits or the constness of any of the traits separately - that has the expected implications for bounds you can then write, but it wouldn't break anything.
tmandry: Just to check, that also applies if we go beyond the current proposal, like something weaker than `Pointee`?
davidtwco: I think so. I discuss the backwards compatibility of introducing new traits in the middle or after the proposed traits in one of the footnotes of the RFC. I think the "can they be stabilised independently" question is the same for those as for the proposed traits.
davidtwco: Also beneficial for the positive bounds approach - to refer to a thing needs it to be stabilised, rather than the next strictest thing being stabilised.
## Compatibility with `AlignSized` / `size != stride` proposals
tmandry: e.g. https://internals.rust-lang.org/t/pre-rfc-allow-array-stride-size/17933
davidtwco: I've read the existing RFCs for `Aligned`, which are discussed briefly in the future possibilities, but unfamiliar with this particular pre-RFC, will need to read up on it.
scottmcm: Obligatory link to <https://lang-team.rust-lang.org/frequently-requested-changes.html#size--stride> where lang has made a fairly strong statement that we wouldn't do this, though not one that couldn't be overridden.
tmandry: Well it would need to be another default bound like here. I would be curious to know how it fits and would like to know we aren't closing any doors.
tmandry: Looking at the `Aligned` proposal I think it's different: The types in question have a known alignment, it's just that they have a "data size" that is not a multiple of that alignment. The proposal is essentially:
- Add `AlignSized` as an implicit bound like `Sized`
- Add `std::mem::data_size_of::<T>()` which is how much space its bytes take up in a struct
- which equals `std::mem::size_of::<T>()` for all `AlignSized` types
- for other types, it may be less, and not include padding bytes
- An array `[T]` continues to use `std::mem::size_of::<T>()` (the stride) between bytes, while struct layouts including these types can be more compact
Let's say we call the "positive" version of `T: ?AlignSized` `T: Compact`. Where does it go here?
```
┌────────────────┐ ┌─────────────────────────────┐
│ const Sized │ ───────────────────────→ │ Sized │
│ {type, target} │ implies │ {type, target, runtime env} │
└────────────────┘ └─────────────────────────────┘
│ │
implies implies
│ │
↓ ↓
┌──────────────────────────────┐ ┌───────────────────────────────────────────┐
│ const MetaSized │ ──────────→ │ MetaSized │
│ {type, target, ptr metadata} │ implies │ {type, target, ptr metadata, runtime env} │
└──────────────────────────────┘ └───────────────────────────────────────────┘
│
implies
│
┌───────────────────────────┘
↓
┌──────────────────┐
│ Pointee │
│ {runtime env, *} │
└──────────────────┘
```
nikomatsakis: Would it be orthogonal? New implied bound is `EquiSized` and could relax to `Compact`.
scottmcm: Don't think a `Compact` type could ever be `MetaSized`, in a world where that means the same as our `?Sized` bounds today, which unsafe code assumes. Could be a hierarchy that we build on top of `Pointee`. We could put it between `Pointee` and `MetaSized`.
tmandry: We could add a new level like `MetaCompactSized` for unsized types that fit in.
nikomatsakis: Don't think we're closing a door but it may not be as expressive as we want.
scottmcm: Want to make sure that relaxing bounds is backwards compatible, no matter what. e.g. if we relax `Box<T: MetaSized>` to `Box<T: Compact>`.
## Way to frame this without const traits?
TC: Const traits may be the right way to frame this, but I wonder whether this can be framed usefully without them in some way. If they could, that would help in decoupling this RFC from another rather hard problem, and might help it to make independent progress.
TC: Perhaps we could always later do some kind of trait aliasing to map a later const trait version to whatever non-const traits we used to model this without that.
davidtwco: I'm open to ideas but I believe that `Copy: Clone: Sized` means that the scalable vectors need to implement `Sized` to implement `Copy` and that can't be relaxed backwards compatibly - constness works around that. I explored doing this without const traits and didn't think it could be done.
TC: Could you elaborate on the "constness works around that" bit? What is it specifically that helps here?
davidtwco: Without constness, `Sized` means "known at compile time", because of `const fn size_of<T>` and that doesn't work for scalable vectors, but we can't relax `Clone: Sized`, so we need to make scalable vectors fit in `Sized`. Constness lets us do that; we are partitioning `Sized` while still being `Sized`.
TC: Does this rely on the conditional `~const` bounds at all, or no?
davidtwco: It benefits from them, `const fn size_of<T: ~const Sized>` allows this to accept scalable vectors at runtime, without the conditional bound that can't happen. But we could do `const fn size_of<T: const Sized>` and just disallow that too. The conditional bounds allow functions to be scalable-vector-agnostic, in other words, while still being const.
TC: It seems to me the conditional bound must be the main thing, because aside from that, I would imagine we could just model this in terms of some other trait with appropriate super-/subtrait relationships or blanket impls. The special thing about the `const` trait work is that conditional bound.
davidtwco: I explored a `RuntimeSized` trait below `Sized` and above `MetaSized` to do this initially (mentioned in footnote), but you can't relax `Clone: Sized` to `Clone: RuntimeSized` for scalable vectors to be `Copy`, it needs to be const. If it didn't have conditional bounds, it would still allow us to implement `Copy`, and prevent scalable vectors being passed anywhere that can be const with a `const Sized` bound, but it is limiting not having the conditional bounds, they serve a useful function.
TC (post-meeting): This discussion continued [in a thread](https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-02-05/near/498167369). Other than the conditional bounds, this can be modeled with normal traits, and David updated the RFC to describe this.
nikomatsakis: I'm becoming less convinced that the default bound should be `const Sized`. It isn't what I think most code needs and it rules out compatibility with some types -- and it's the only "const" bound that you get by default. I feel like the more it aligns with "regular const" the less annoying it will be. But overall `const SizeOfVal` doesn't seem like a terrifying idea, it just says "I can call `size_of_val` at const-time".
scottmcm: One counterpoint that comes to mind is inside the standard library... something with ZSTs.
TC: I think of runtime `Sized` types like SVEs as us querying the processor to get the size. The RFC calls these something like "statically known at runtime". But I wonder whether this notion is extensible. We could instead, e.g., be querying a network API at startup to get the size of some type.
tmandry: It means you can generate some code as if the size was statically known.
TC: Let's talk through how this depends on const trait impls exactly.
davidtwco:
```rust
trait Foo: Clone {
fn uses_supertrait_bound() { requires_sized::<Self>() }
}
fn requires_sized<T: Sized>() { /* ... */ }
// if you changed
trait Clone: Sized
// to
trait Clone: RuntimeSized
// above no longer compiles
impl Copy for svint8_t {} // necessary, implies svint8_t Sized
//impl Sized for svint8_t // Does not do this.
impl RuntimeSized for svint8_t {}
impl Sized for svint8_t {}
// does not:
//impl const Sized for svint8_t
trait ConstSized: Sized {}
// do not:
//impl ConstSized for svint8_t {}
// svint8_t can't be Sized, Sized requires compile time
fn do_stuff<T: Sized>(value: T) { size_of(value) }
// T: const Sized
// const fn size_of<T: Sized> --> const fn size_of<T: ConstSized>
// const fn size_of<T: Sized> --> const fn size_of<T: const Sized>
```
(The meeting ended here.)
---
## Thoughts on the various `Pointee`s
scottmcm: I'm reminded of [ACP: replace use of Pointee trait with a ptr::Metadata type #246](https://github.com/rust-lang/libs-team/issues/246) when I see the notes about `Pointee`. Would something like that be beneficial to the proposal?
davidtwco: I think we'd be compatible with that. We avoid reusing `ptr::Pointee` because adding it as a supertrait of `Sized` means `T::Metadata` is ambigious, otherwise it represents approximately the same thing. If it was a type then we could still add a marker trait without the associated type like we're proposing now w/ whatever name we wanted.
## Thoughts on SVE
tmandry: Okay to leave out of scope, but: How are we really gonna deal with SVE types? They can't go in structs or spill to the stack?
davidtwco: my colleague at Arm has an RFC proposing `repr(scalable)`, it may address these points - https://github.com/rust-lang/rfcs/pull/3268 - but it needs this RFC as a foundation to avoid type system special-casing. I've mostly been focusing on this part so don't recall how we address those specific concerns.
## niko jots down notes
nikomatsakis: I want to drop down notes here. I'll try to coallesce these into *questions* later, if needed.
Aligned: I still think the "basic shape" of this RFC feels right to me. I like the use of `const` and I like moving away from `?Trait` towards "name the special trait that you want / need".
Bikeshed: I do not like the name `MetaSized`. It is difficult for me to remember what it is. Basically any time people use the term "meta" I feel confused. I wonder what other names have been considered. To me the salient detail is it is something where the *type* does not have a size, but *values* of the type do -- so perhaps `SizedVal` or `SizedValue`? This also aligns with "you can call `size_of_val`".
Observation: Writing `const Sized` does feel a *bit* verbose. I think it's probably ok but I could imagine introducing an alias like `trait StaticSized = const Sized`. Alternatively, perhaps we make `Sized` be that alias and create a new name (say, `SizedType`) and then change the definition of `trait Clone: Sized` to be `trait Clone: SizedType`. I think if we do that atomically it should be ok? (Hmm, is there an interaction around backwards compatibility here...?)
*Question:* Today, I can write this on stable...
```rust
const fn foo<T: Sized>() {
let c = size_of::<T>();
}
```
...but shouldn't it (per this RFC) require `T: const Sized`? Am I missing something? Oh, it's done through the magic of editions... "In the current edition, all existing Sized bounds will be syntatic sugar for const Sized (both explicitly written and implicit)." *Got it.*
But today this compiles too...
```rust
const fn foo<T: Clone + ?Sized>() {
let c = size_of::<T>();
}
```
...so it seems as if the `Clone` trait would have to be changed to have `const Sized` as a supertrait bound, right? Or else are we dealing with that in some way?
On the interaction with `const` traits: I do not think we should block accepting the RFC on accepting const traits. I do think we should add an unresolved question to revisit the relationship between them, and I would of course be reluctant to *stabilize* without having an accepted const trait RFC.
Question: I *think* that although "In practice, every type will implement Pointee", one thing I like about this proposal is that we can in fact change this later. We could add a new layer below Pointee (say, used to indicate logical values that reside on the GPU and hence do not have an address or something) -- all existing code would either have the default bound or would have explicitly written `T: Pointee`, so only new code that names `T: NewTraitName` (whatever that new trait name is) would be impacted. Of course we may find that some existing functions ought to have looser bounds, but I believe that is in principle achievable as well.
*Question:* Supertrait bound: "It is necessary to introduce a implicit default bound of const MetaSized on a trait's Self type in order to maintain backwards compatibility?" Should we make this tied to the edition? Hmm. I'm of two minds. On the one hand, I rather like the idea that trait `Self` types are as general as they can be by default, but on the other hand, this undermines my statement that we can add a new "level" of types.
*Question:* "This is forward compatible with trait bounds which have sizedness supertraits implying the removal of the default const Sized bound." -- it is?? I rather think that supertrait bounds *should* be special, but I don't want to tie this into this RFC, not sure how to think about that. I think to be truly compatible you'd have to require `T: Foo + MetaSized` be written explicitly.