only bounds are going to be the most impactful change to Rust that you’ve never heard of. They are currently being designed and developed by the Arm team (David Wood, Rémy Rakic, et al.) as part of the . Before that, &[u32] was actually a built-in, indivisible type; we even wrote it like [u32]/& for a time.
Problem: ?Sized notation doesn’t scale to this hierarchy
But now we have a kind of problem. The ?Sized notation was predicated. Note that this is a distinct feature and is not covered under the . In sync code, this works because we’ve decided it’s UB to unwind a stack frame without running the destructors of values stored there, and so if you put a local variable on the stack, you can be sure its destructor will run. But that doesn’t work in async code! And there are times when unwinding without running destructors would be nice.
The solution is to introduce a second family of default traits. Unlike the Sized family we saw before, this family defines fine-grained capabilities about how values of that type can be used:
flowchart TD
subgraph A["Accessability traits"]
Forget[["Forget (default)"]] -- extends --> Leak
Leak -- extends --> Destruct
Destruct -- extends --> Access
Move[["Move (default)"]] -- extends --> Access
end
Copy -- extends --> Move
The meaning of the traits are as follows:
Forget, the default, says that you can recycle the memory for a value without running its destructor.Leaksays that you can skip running a destructor for a value, but only if you never reuse the memory where the value resides.Destructsays that if you have a value of this type, you can reuse the memory where it resides by running its destructor.Copy, which already exists, says that you can memcpy the place and keep using the original place; it’s not really a default, but I included it because it is relevant.Move, another default, says that you can memcpy the value to a new place if you stop using the original.Accessis the root of this family. It indicates a value that can be “accessed in place” (basically, any value at all).
This introduces new checks into the compiler:
- When you move a value (i.e.,
a = bwherebis not used later), we will check that the type implementsMove(whereas today, it is always allowed). - When you exit a scope, we will check that the values in each local variables have either been moved or have a type that implements
Destruct.
Some implications:
- If your function owns a value of type
T: only Destruct, then you must destruct it before your function returns. You can’t move it (because you don’t know if it implementsMove) and you can’t leak or forget it either. - If your function owns a value of type
T: only Move, then the only thing you can do with it is move it somewhere else. You can’t drop it (because you don’t know if it implementsDestruct). - No function can own a value of type
T: only Access, because you wouldn’t be able to move it nor drop it, and hence you could not return. But you could have such a value (say) in astatic.
How only bounds could work in the presence of multiple families
The spur for writing this blog post was a question in a lang team meeting on how only bounds ought to work given the existence of multiple “families” of default traits, as I described above. Although the would allow for this definition to omit the U: only Move bound because we could statically guarantee that the Option will be constructed in place and never moved after that.
Option::or requires only Move + Destruct
The a.or(b) method on Option returns a if it is Some and otherwise returns b. This is an interesting one because the value b may not be used and therefore requires only Move + Destruct bounds.
impl<T: only Move> Option<T> {
fn or(
self,
alternate: Option<T>,
) -> Option<T>
where
T: Destruct, // <-- because it may be dropped
{
match self {
Some(v) => Some(v), // drops `alternate`
None => alternate, // moves `alternate`
}
}
}
Rc requires MaybeSized + Leak
The Rc type is an example where we would want to relax bounds from both families:
struct Rc<T: only MaybeSized + only Leak> {}
I believe the proper minimum bounds for Rc are:
only MaybeSizedbecause while it can storeMetadataSizedorSizedthings, it doesn’t have to, it can also store things of an non-computable size (although it does raise the question of how they would be freed, but that’s an allocator concern).only LeakbecauseRcvalues can form cycles and thus we can’t ever guarantee the destructor will be run. Interestingly,Rc<T>can implementForgeteven its contents don’t.
Frequently asked questions
What is actually under RFC today?
The post may be a bit confusing here. The has a lot of details. The latest updates are available on the .
Conclusion
I want to close with a meta-observation and a big shout-out to the Arm team. I think they are showing how awesome open-source can be. The Arm team’s primary motivation is adding support for Scalable Vector Extensions. This helps Rust make full use of Arm processors. This is, in and of itself, a laudable goal, and valuable to Rust: One of Rust’s assets, in my view, is that it gives you access to all the power your processor has to provide, and that should include unique extensions.
But rather than add the feature as a kind of special-case extension to Rust, the Arm team is going further and driving a general purpose improvement, one that will unlock a bunch of other features (extern types and, to some extent, guaranteed destructors; guaranteed destructores themselves unlock scoped async threads and better Wasm integration). I love that.
In fact, I recall that in one of my blog posts I proposed writing
""as the way to spell&str. I kinda wish we had done that just for the sheer wackiness of it (fn foo(name: "")).
Little logic pun there for you. ↩︎
SOCIAL SHARE CARD GENERATOR