This post describes a new language feature, “baseline bounds”, which would enable new flavors of less-bounded type variables like ?Sized
, in a way that is comprehensible and extensible to future needs rather than adding more special cases to the language. I do not think it is a good language feature by itself, but it might be a solution to language design problems in several other proposed features.
I already posted it on RFC 3729’s discussion but afterward, I thought it should be put in a more findable location than one PR comment.
Background
In Rust, every type parameter has bounds (fn foo<T: Trait>(...
) which specify what can be done with it, but it also has a set of default assumptions about what can be done with it, that are not described by bounds. A type parameter with no bounds written allows the value to:
- be moved, dropped, or swapped
- be forgotten with
mem::forget
- have its size measured with
mem::size_of
If the special bound T: ?Sized
is added, then it is no longer possible to use size_of()
, only size_of_val()
, allowing dynamically sized types.
Occasionally there are proposals to introduce new functionality into the language which would require being able to have types which don’t support one of these things — being not movable (replacements for Pin
), not forgettable (linear types), or not having an even dynamically known size (extern types, thin DSTs[1]).
The catch is that, since changing what Sized
means is a breaking change, this would require introducing more things like ?Sized
— but these anti-bounds are confusing and break the simple model that T: Foo + Bar
means you can do Foo
things and Bar
things, so if there was more than one of them it would be unclear exactly how they interact. People have said the language team is reluctant to add more ?
bounds despite how useful they would be for some things.
I have a idea for, if such weakenings are included, they can be provided in a way which avoids creating confusion and is extensible for future needs. I am not proposing this be added by itself to Rust — I’m just sharing it in case it is useful to further language design work that needs a solution to Sized
being too one-size-fits-all or ?Sized
being still too strong. This is not a pre-RFC.
Definition
There would be a new type of bound on a type parameter, known as a “baseline bound”.
-
Let's say the syntax is
T: @Trait
. (Symbol subject to bikeshedding, of course, but we can think of it as “begin @ this point”; it could also perhaps be given a contextual keyword if there is a good short one to be found.) -
A trait can only be used in a baseline bound if any of the following are true:
-
It is the
Sized
trait. -
It is a new trait the standard library provides to replace
?Sized
bounds, i.e. which allows everything that you can do with any type in current Rust — let's suppose this is calledNotSized
, as a deliberately bad placeholder name. (The actual name of any such new trait should should be chosen to suit whatever extensions of the language are being added along with baseline bounds to support them.) -
It is a user-defined trait whose supertrait bounds include a baseline bound, such as
trait Foo: @Sized {}
This constraint ensures that it’s impossible to write a type variable that has truly zero bounds, so new weakenings are always possible, and a trait can’t be used for its own baseline unless it opts in to offering that. (Note in particular that traits today essentially have
?Sized
.)Changing
trait Foo: Sized {}
totrait Foo: @Sized {}
is a non-breaking change.
-
-
Every type parameter has exactly one baseline bound.
- If none is explicitly written, it is given
@Sized
as the implicit baseline bound; future editions may change this to another trait. - If two are written, this is an error.
- If none is explicitly written, it is given
-
Every trait has a baseline bound on
Self
(supertrait bound). If none is explicitly written, it is given@NotSized
(equivalent to today’s?Sized
). -
Every associated type of a trait has a baseline bound. If none is explicitly written, it is given
@Sized
. -
Baseline bounds do not grant any functionality beyond ordinary bounds. Therefore,
T: @Foo + Bar
is completely equivalent toT: @Bar + Foo
, if bothFoo
andBar
are permitted in baseline position at all.
Examples
T
with no bounds is equal toT: @Sized
in current editions (2015-2024). In a future edition, it could be made to mean something different.T: Sized
is equal toT: Sized + @Sized
, hence justT: @Sized
, in current editions. Useless and harmless today, still harmless in the future.T: @Sized
is “the only bound isSized
, regardless of the current edition” — it is redundant in current editions but would be added as part of automatic migration to a future edition that changed the implicit choice of baseline bound.T: @NotSized
is equal to today’sT: ?Sized
, in all editions.T: ?Sized
has the 2015-2024 meaning forevermore; users may choose to replace it withT: @NotSized
.T: @SomeOtherTrait
is an error unlessSomeOtherTrait
has an@
bound as a supertrait.T: ?SomeOtherTrait
does nothing and issues a warning, as today.
Benefits
-
Every trait that can be used as a baseline tells you what you can do with that type parameter, as an explicit item provided by a library. The language no longer privileges
Sized
uniquely (except for compatibility). Future versions of Rust can introduce new weaker or stronger baseline traits without introducing new syntax or new trait bound semantics. -
Compared to language design proposals where a syntactically ordinary trait bound is given a special meaning (i.e.
T: NotSized
orT: Move
), the presence of a symbol tells you that something special is going on, and hopefully the documentation of the trait will tell you what exactly that means in the case of that trait. -
We can, if we wish, choose to stop using
?Sized
entirely, so that all explicitly-written bounds are positive — they tell you the name for what you can do, not the name for what you can’t. -
All code that uses an
@
bound is now edition-change-proof; it has picked a named baseline and has opted out of all implicit bounds. This simplifies language evolution questions to the separate choices,- “is there room to split off a new weaker trait to serve this need?”
- “what should the implicit baseline bound for the next edition be?”
and we no longer need to ask “should there be more
?
bounds” or “should there be new bound semantics or syntax”; because the baseline is named, there’s lots of room to add more baseline options as necessary without saying “this one is the correct one; we definitely got it right this time”. -
User-defined baseline traits can make code more concise;
T: @Foo
express “this is bounded byFoo
,Foo
’s supertraits, and nothing else”; thus, it simplifies use-cases where one must today write<T: ?Sized + Foo>
in every generic parameter. -
Users who wish to write libraries which are compatible with, but do not require, dynamically-sized types (or extern types or other weakenings in the future) could choose a style (perhaps enforced by a Clippy restriction lint) where all type variables have an explicit baseline bound, forcing them to remember to choose between
@Sized
or@NotSized
for each type parameter.
Drawbacks
- More syntax.
- Two ways to write
T: ?Sized
. - It might be that the extensibility this enables would be significantly bad for the language’s comprehensibility, and Rust should instead continue to not do any of the things it enables.
Closing remarks
Baseline bounds can be seen as similar to language editions, in that they allow incremental change without introducing new incompatible versions, by giving a name to “what set of default assumptions you are using”, so the name can be chosen explicitly, have documentation, et cetera.
Again, I am not proposing that this be added to Rust by itself. If you’re thinking “this doesn’t pull its weight” — yes, I agree, it isn’t worth doing unless it is justified by unblocking other valuable features. This is not a pre-RFC, just writing down an idea for reference.
like a
CStr
defining its length by actually looking for the zero byte ↩︎