---
title: "Design meeting 2025-01-15: Const trait impls"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2025-01-15
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-01-15
url: https://hackmd.io/rF-h-9e3StGZzKMMsSLwCw
---
# T-lang design meeting on const traits
The RFC can be found [here](https://github.com/rust-lang/rfcs/pull/3762) (and below).
To focus the meeting, here are some topics to keep in mind while reading it.
## Questions that need an answer
### Stabilize this sugar before knowing many details about future effects work.
This RFC is forward compatible to changing it into sugar of a future effect system. All the effect systems discussed so far are significantly more powerful than anything const traits can do. We (Yosh, TC, and I (oli-obk)) will brainstorm some more about these and likely write some (non-proposal) blog posts that show how that could be done.
I would like there to be a decision for shipping this const traits sugar (modulo any syntax bikeshedding, see below), as I strongly believe we want this sugar anyway, and that it is rusty and easily digestible by users.
### Do not consider an opt-out bound
The old RFC proposed `T: Trait` to be callable in const contexts and `T: ?const Trait` to be what is `T: Trait` today: methods are not callable, but you can refer to them, cast them to function pointers or just access assoc types and consts.
We got this wrong last time (see https://github.com/rust-lang/rust/issues/83452), because opt-out bounds are hard.
Similarly we keep getting `?Sized` wrong: https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209.
While it's "just" a syntax change to now flip the logic in the parser to produce the same internal representation that we have now, this creates a maintenance hazard imo.
Additionally, just from the lang side, having an opt-in instead of an opt-out bound nudges function authors to using the minimal necessary bounds to get their function
body to compile and thus requiring as little as possible from their callers.
Such a change also requires an edition change to all `const fn` out there in addition to shifting how people think about const fn, even though they have been used to it now on stable for years.
I do not believe the ecosystem churn is worth changing anything here, either with parallel syntax (new `~const fn`s) or with an edition change.
### Where goes the sigil?
Options (old RFC is not an actual option (without an edition, or at all in oli's opinion, see the above section for details), only listed for completeness):
| option | not const | conditionally const | always const |
| ------------ | ------------------ | ------------------- | ----------------- |
| RFC | `Trait` | `~const Trait` | `const Trait` |
| ~~old RFC~~ | `?const Trait` | `Trait` | unresolved |
| Josh | `Trait` | `const Trait` | `=const Trait` |
| Niko | `Trait` | `const Trait` | postpone \(\*\) |
I think we should have an always const syntax, as it's immensely useful for const blocks and will allow ppl to stop building workarounds with associated consts. Also, having it in bounds is the extent of the always-const feature, there's not really much interesting in the feature (and its impl) beyond syntax.
\(\*\) Talking to TC gave me a thought or two here, I'll drop something in the questions below. --nikomatsakis
### Do we mark all methods with `const`?
We could require all methods to have `const` in front of them, instead of implying it. The RFC does not propose this as I feel it's just syntactical noise as the trait is already marked.
```rust
const trait Foo {
const fn foo(&self);
const fn bar(&self);
}
```
It would make per-method opt-in easier, see next section (but per-method opt-in is not necessarily useful).
But it makes it much harder/more confusing to add
```rust
trait Tr {
const C: u8 = Self::f();
const fn f() -> u8;
}
```
later. Thus we should imo just keep the RFC proposal.
## Things that can be unresolved questions...
...to be resolved before stabilization or even after
### Per-method opt-in/opt-out
The RFC proposes to make all methods on a trait be `const` and thus callable from a const context.
Once we have some usage on stable, we evaluate whether people are looking for an opt-out and figure out how to add one. One solution is an attribute, considering that it's assumed to be rarely needed anyway.
Most of those rare cases can split the trait into a const trait and a non-const trait, we do not expect this to be something core ecosystem traits would want and we have zero examples in libstd.
The closest thing we have is `Iterator` being hard to transform into a const trait in one go and we would like to just have a forever unstable option to slowly do it.
Another way to do it would be to allow
```rust
const trait Foo {
const fn foo(&self);
fn bar(&self);
}
```
where `bar` is not required to be const in impls, and is not callable on `T: ~const Foo` bounds. Impls can still impl `bar` as `const` as a refinement, and are required to implement `foo` as `const` obviously.
The issue with such a system is that changing a non-const method to a const method is a breaking change for users of that trait, as they may already have a const impl that doesn't make that method const, but now that is required.
```rust
// crate a
const trait Foo {
const fn foo(&self);
fn bar(&self);
}
// crate b
impl const a::Foo for MyStruct {
const fn foo(&self) { }
fn bar(&self) { write_to_disk() }
}
```
There's no way to make turning `Foo::bar` into a const fn work. Either we break crate b by making it require `const` in front of `fn bar` now, or we break users of `MyStruct` who pass it to const fns expecting a `const a::Foo` (if we made the constness of the impl depend on all const fns being impled as const fn).
Since a breaking change is needed for something like this *anyway* it may be simpler to require folk to split their traits. This would make it annoying in the rare cases (we have found zero real world cases where this would be desirable, and we looked extensively), but have a very simple model for the usual case.
Furthermore everywhere else we're saying "adding `const` to the function, trait or impl does not break things", so diverging from that by having `const` markers on methods, too, would be misleading.
### `~const Destruct` bounds.
There's a lot of ways to go about them, but nothing where there's any clearly optimal way. so we'll just go either with all const traits having an implicit `~const Destruct` super trait or all `~const` trait bounds having an implicit `~const Destruct` bound. This will require very few manual `~const Destruct` bounds, and only rarely add such a bound where the user didn't want it.
---
- Feature Name: `const_trait_methods`
- Start Date: 2024-12-13
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#67792](https://github.com/rust-lang/rust/issues/67792)
# Summary
[summary]: #summary
Make trait methods callable in const contexts. This includes the following parts:
* Allow marking `trait` declarations as const implementable.
* Allow marking `trait` impls as `const`.
* Allow marking trait bounds as `const` to make methods of them callable in const contexts.
Fully contained example ([Playground of currently working example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2ab8d572c63bcf116b93c632705ddc1b)):
```rust
const trait Default {
fn default() -> Self;
}
impl const Default for () {
fn default() {}
}
const fn default<T: ~const Default>() -> T {
T::default()
}
fn compile_time_default<T: const Default>() -> T {
const { T::default() }
}
const _: () = Default::default();
fn main() {
let () = default();
let () = compile_time_default();
let () = Default::default();
}
```
# Motivation
[motivation]: #motivation
Const code is currently only able to use a small subset of Rust code, as many standard library APIs and builtin syntax things require calling trait methods to work.
As an example, in const contexts you cannot use even basic equality on anything but primitives:
```rust
const fn foo() {
let a = [1, 2, 3];
let b = [1, 2, 4];
if a == b {} // ERROR: cannot call non-const operator in constant functions
}
```
## Background
This RFC requires familarity with "const contexts", so you may have to read [the relevant reference section](https://doc.rust-lang.org/reference/const_eval.html#const-context) first.
Calling functions during const eval requires those functions' bodies to only use statements that const eval can handle. While it's possible to just run any code until it hits a statement const eval cannot handle, that would mean the function body is part of its semver guarantees. Something as innocent as a logging statement would make the function uncallable during const eval.
Thus we have a marker (`const`) to add in front of functions that requires the function body to only contain things const eval can handle. This in turn allows a `const` annotated function to be called from const contexts, as you now have a guarantee it will stay callable.
When calling a trait method, this simple scheme (that works great for free functions and inherent methods) does not work.
Throughout this document, we'll be revisiting the example below. Method syntax and `dyn Trait` problems all also exist with static method calls, so we'll stick with the latter to have the simplest examples possible.
```rust
const fn default<T: Default>() -> T {
T::default()
}
// Could also be `const fn`, but that's an orthogonal change
fn compile_time_default<T: Default>() -> T {
const { T::default() }
}
```
Neither of the above should (or do) compile.
The first, because you could pass any type T whose default impl could
* mutate a global static,
* read from a file, or
* just allocate memory,
which are all not possible right now in const code, and some can't be done in Rust in const code at all.
It should be possible to write `default` in a way that allows it to be called in const contexts
for types whose `Default` impl's `default` method satisfies all rules that `const fn` must satisfy
(including some annotation that guarantees this won't break by accident).
It must always be possible to call `default` outside of const contexts with no limitations on the generic parameters that may be passed.
Similarly it should be possible to write `compile_time_default` in a way that also requires calls
outside of const contexts to only pass generic parameters whose `Default::default` method satisifies
the usual `const fn` rules. This is necessary in order to allow a const block
(which can access generic parameters) in the function body to invoke methods on the generic parameter.
So, we need some annotation that differentiates a `T: Default` bound from one that gives us the guarantees we're looking for.
# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation
## Nomenclature and new syntax concepts
### Const trait impls
It is now allowed to prefix a trait name in an impl block with `const`, marking that this `impl`'s type is now allowed to
have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like ususal, but more on this later).
An example looks as follows:
```rust
impl const Trait for Type {}
```
Such impls require that the trait is a `const trait`.
All method bodies in a const trait impl are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context).
### Const traits
Traits need to opt-in to being allowed to have const trait impls. Thus you need to declare your traits by prefixing the `trait` keyword with `const`:
```rust
const trait Trait {}
```
This in turn checks all methods' default bodies as if they were `const fn`, making them callable in const contexts.
Impls can now rely on the default methods being const, too, and don't need to override them with a const body.
We may add an attribute later to allow you to mark individual trait methods as not-const so that when creating a const trait, one can
add (defaulted or not) methods that cannot be used in const contexts.
It is possible to split up a trait into the const an non-const parts as discussed [here](#cant-have-const-methods-and-nonconst-methods-on-the-same-trait).
All default method bodies of const trait declarations are [const contexts](https://doc.rust-lang.org/reference/const_eval.html#const-context).
Note that on nightly the syntax is
```rust
#[const_trait]
trait Trait {}
```
and a result of this RFC would be that we would remove the attribute and add the `const trait` syntax.
### Const trait bounds
Any item that can have trait bounds can also have `const Trait` bounds.
Examples:
* `T: const Trait`, requiring any type that `T` is instantiated with to have a const trait impl.
* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a const trait impl.
* These are not part of this RFC because they require `const fn` function pointers. See [the Future Possibilities section](#future-possibilities).
* `impl const Trait` (in all positions).
* These are not part of this RFC because they require `const fn` function pointers. See [the Future Possibilities section](#future-possibilities).
* `trait Foo: const Bar {}`, requiring every type that has an impl for `Foo` (even a non-const one), to also have a const trait impl for `Bar`.
Such an impl allows you to use the type that is bound within a const block or any other const context, because we know that the type has a const trait impl and thus
must be executable at compile time. The following function will invoke the `Default` impl of a type at compile time and store the result in a constant. Then it returns that constant instead of computing the value every time.
```rust
fn compile_time_default<T: const Default>() -> T {
const { T::default() }
}
```
### Conditionally-const trait bounds
Many generic `const fn` and especially many const trait impls do not actually require a const trait impl for their generic parameters.
As `const fn` can also be called at runtime, it would be too strict to require it to only be able to call things with const trait impls.
Picking up the example from [the beginning](#summary):
```rust
const trait Default {
fn default() -> Self;
}
impl const Default for () {
fn default() {}
}
impl<T: Default> Default for Box<T> {
fn default() -> Self { Box::new(T::default()) }
}
// This function requires a `const` impl for the type passed for T,
// even if called from a non-const context
const fn default<T: const Default>() -> T {
T::default()
}
const _: () = default();
fn main() {
let _: Box<u32> = default();
//~^ ERROR: <Box<u32> as Default>::default cannot be called at compile-time
}
```
What we instead want is that, just like `const fn` can be called at runtime and compile time, we want their trait bounds' constness
to mirror that behaviour. So we're introducing `~const Trait` bounds, which mean "const if called from const context" (slight oversimplifcation, but read on).
The only thing we need to change in our above example is the `default` function, changing the `const Default` bound to a `~const Default` one.
```rust
const fn default<T: ~const Default>() -> T {
T::default()
}
```
`~const` is derived from "approximately", meaning "conditionally" in this context, or specifically "const impl required if called in const context".
It is the opposite of `?` (prexisting for `?Sized` bounds), which also means "conditionally", but from the other direction: `?const` (not proposed here, see the alternatives section for why it was rejected) would mean "no const impl required, even if called in const context".
See [this alternatives section](#make-all-const-fn-arguments-const-trait-by-default-and-require-an-opt-out-const-trait) for an explanation of why we do not use a `?const` scheme.
### Const fn
`const` fn have always been and will stay "always const" functions.
It may appear that a function is suddenly "not a const fn" if it gets passed a type that doesn't satisfy
the constness of the corresponding trait bound. E.g.
```rust
struct Foo;
impl Clone for Foo {
fn clone(&self) -> Self {
Foo
}
}
const fn bar<T: ~const Clone>(t: &T) -> T { t.clone() }
const BAR: Foo = bar(Foo); // ERROR: `Foo`'s `Clone` impl is not for `const Clone`.
```
But `bar` is still a `const` fn and you can call it from a const context, it will just fail some trait bounds. This is no different from
```rust
const fn dup<T: Copy>(a: T) -> (T, T) {(a, a)}
const FOO: (String, String) = dup(String::new());
```
Here `dup` is always const fn, you'll just get a trait bound failure if the type you pass isn't `Copy`.
This may seem like language lawyering, but that's how the impl works and how we should be talking about it.
It's actually important for inference and method resolution in the nonconst world today.
You first figure out which method you're calling, then you check its bounds.
Otherwise it would at least seem like we'd have to allow some SFINAE or method overloading style things,
which we definitely do not support and have historically rejected over and over again.
### `~const Destruct` trait
The `Destruct` trait enables dropping types within a const context.
```rust
const fn foo<T>(t: T) {
// `t` is dropped here, but we don't know if we can evaluate its `Drop` impl (or that of its fields' types)
}
const fn baz<T: Copy>(t: T) {
// Fine, `Copy` implies that no `Drop` impl exists
}
const fn bar<T: ~const Destruct>(t: T) {
// Fine, we can safely invoke the destructor of `T`.
}
```
When a value of a generic type goes out of scope, it is dropped and (if it has one) its `Drop` impl gets invoked.
This situation seems no different from other trait bounds, except that types can be dropped without implementing `Drop`
(as they can contain types that implement `Drop`). In that case the type's drop glue is invoked.
The `Destruct` trait is a bound for whether a type has drop glue. This is trivally true for all types.
`~const Destruct` trait bounds are satisfied only if the type's `Drop` impl (if any) is `const` and all of the types of
its components are `~const Destruct`.
While this means that it's a breaking change to add a type with a non-const `Drop` impl to a type,
that's already true and nothing new:
```rust
pub struct S {
x: u8,
y: Box<()>, // adding this field breaks code.
}
const fn f(_: S) {}
//~^ ERROR destructor of `S` cannot be evaluated at compile-time
```
## Trivially enabled features
You can use `==` operators on most types from libstd from within const contexts.
```rust
const _: () = {
let a = [1, 2, 3];
let b = [4, 5, 6];
assert!(a != b);
};
const _: () = {
let a = Some(42);
let b = a;
assert!(a == b);
};
```
Note that the use of `assert_eq!` is waiting on `Debug` impls becoming `const`, which
will likely be tracked under a separate feature gate under the purview of T-libs.
Similarly other traits will be made `const` over time, but doing so will be
unblocked by this feature.
## Crate authors: Making your own custom types easier to use
You can write const trait impls of many standard library traits for your own types.
While it was often possible to write the same code in inherent methods, operators were
covered by traits from `std::ops` and thus not avaiable for const contexts.
Most of the time it suffices to add `const` before the trait name in the impl block.
The compiler will guide you and suggest where to also
add `~const` bounds for trait bounds on generic parameters of methods or the impl.
Similarly you can make your traits available for users of your crate to implement constly.
Note that this will change your semver guarantees: you are now guaranteeing that any future
methods you add don't just have a default body, but a `const` default body. The compiler will
enforce this, so you can't accidentally make a mistake, but it may still limit how you can
extend your trait without having to do a major version bump.
Most of the time it suffices to add `const` before the `trait` declaration. The compiler will
guide you and suggest where to also add `~const` bounds for super trait bounds or trait bounds
on generic parameters of your trait or your methods.
# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation
## How does this work in the compiler?
These `const` or `~const` trait bounds desugar to normal trait bounds without modifiers, plus an additional constness bound that has no surface level syntax.
A much more detailed explanation can be found in https://hackmd.io/@compiler-errors/r12zoixg1l#What-now
We generate a `ClauseKind::HostEffect` for every `const` or `~const` bound.
To mirror how some effectful languages represent such effects,
I'm going to use `<Type as Trait>::k#host` to allow setting whether the `host` effect is "const" (disabled) or "conditionally" (generic).
This is not comparable with other associated bounds like type bounds or const bounds, as the values the associated host effect can
take do neither have a usual hierarchy nor a concrete single value we can compare due to the following handling of those bounds:
* There is no "always" (enabled), as that is just the lack of a host effect, meaning no `<Type as Trait>::k#host` bound at all.
* In contrast to other effect systems, we do not track the effect as a true generic parameter in the type system,
but instead just ignore all `Conditionally` bounds in host environments and treat them as `Const` in const environments.
While this could be modelled with generic parameters in the type system, that:
* Has been attempted and is really complex (fragile) on the impl side and on the reasoning about things side.
* Appears to permit more behaviours than are desirable (multiple such parameters, math on these parameters, ...), so they need to be prevented, adding more checks.
* Is not necessary unless we'd allow much more complex kinds of bounds. So it can be kept open as a future possibility, but for now there's no need.
* Does not quite work in Rust due to the constness then being early bound instead of late bound, cause all kinds of problems around closures and function calls.
* Technically cause two entirely separate MIR bodies to be generated, one for where the effect is on and one where it is off. On top of that it then theoretically allows you to call the const MIR body from non-const code.
Thus that approach was abandoned after proponents and opponents cooperated in trying to make the generic parameter approach work, resulting in all proponents becoming opponents, too.
### `const` desugaring
```rust
fn compile_time_default<T: const Default>() -> T {
const { T::default() }
}
```
desugars to
```rust
fn compile_time_default<T>() -> T
where
T: Default,
<T as Default>::k#host = Const,
{
const { T::default() }
}
```
### `~const` desugaring
```rust
const fn default<T: ~const Default>() -> T {
T::default()
}
```
desugars to
```rust
const fn default<T>() -> T
where
T: Default,
<T as Default>::k#host = Conditionally,
{
T::default()
}
```
### Why not both?
```rust
const fn checked_default<T>() -> T
where
T: const Default,
T: ~const Default,
T: ~const PartialEq,
{
let a = const { T::default() };
let b = T::default();
if a == b {
a
} else {
panic!()
}
}
```
Has a redundant bound. `T: const Default` implies `T: ~const Default`, so while the desugaring will include both (but may filter them out if we deem it useful on the impl side),
there is absolutely no difference (just like specifying `Fn() + FnOnce()` has a redundant `FnOnce()` bound).
## Precedence of `~`
The `~` sigil applies to the `const`, not the `const Trait`, so you can think of it as `(~const) Trait`, not `~(const Trait)`.
This is both handled this way by the parser, and semantically what is meant here. The constness of the trait bound is affected,
the trait bound itself exists either way.
## Why do traits need to be marked as "const implementable"?
### Default method bodies
Adding a new method with a default body would become a breaking change unless that method/default body
would somehow be marked as `const`, too. So by marking the trait, you're opting into the requirement that all default bodies are const checked,
and thus neither `impl const Trait for Type` items nor `impl Trait for Type` items will be affected if you add a new method with a default body.
This scheme avoids adding a new kind of breaking change to the Rust language,
and instead allows everyone managing a public trait in their crate to continue relying on the
previous rule "adding a new method is not a breaking change if it has a default body".
### `~const Destruct` super trait
Traits that have `self` (by ownership) methods, will almost always drop the `self` in these methods' bodies unless they are simple wrappers that just forward to the generic parameters' bounds.
The following never drops `T`, because it's the job of `<T as Add>` to handle dropping the values.
```rust
struct NewType<T>(T);
impl<T: ~const Add<Output = T>> const Add for NewType<T> {
type Output = Self,
fn add(self, other: Self) -> Self::Output {
NewType(self.0 + other.0)
}
}
```
But if any code path could drop a value...
```rust
struct NewType<T>(T, bool);
struct Error;
impl<T: ~const Add<Output = T>> const Add for NewType<T> {
type Output = Result<Self, Error>;
fn add(self, other: Self) -> Self::Output {
if self.1 {
Ok(NewType(self.0 + other.0, other.1))
} else {
// Drops both `self.0` and `self.1`
Err(Error)
}
}
}
```
... then we need to add a `~const Destruct` bound to `T`, to ensure
`NewType<T>` can be dropped.
This bound in turn will be infectious to all generic users of `NewType` like
```rust
const fn add<T: ~const Add>(
a: NewType<T>,
b: NewType<T>,
) -> Result<NewType<T::Output>, Error> {
a + b
}
```
which now need a `T: ~const Destruct` bound, too.
In practice we have noticed that a large portion of APIs will have a `~const Destruct` bound.
This bound has little value as an explicit bound that appears almost everywhere.
Especially since it is a fairly straight forward assumption that a type that has const trait impls will also have a `const Drop` impl or only contain `const Destruct` types.
Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere.
We may offer an opt out of this behaviour in the future, if there are convincing real world use cases.
### `~const` bounds on `Drop` impls
It is legal to add `~const` to `Drop` impls' bounds, even thought the struct doesn't have them:
```rust
const trait Bar {
fn thing(&mut self);
}
struct Foo<T: Bar>(T);
impl<T: ~const Bar> const Drop for Foo<T> {
fn drop(&mut self) {
self.0.thing();
}
}
```
There is no reason (and no coherent representation) of adding `~const` trait bounds to a type.
Our usual `Drop` rules enforce that an impl must have the same bounds as the type.
`~const` modifiers are special here, because they are only needed in const contexts.
While they cause exactly the divergence that we want to prevent with the `Drop` impl rules:
a type can be declared, but not dropped, because bounds are unfulfilled, this is:
* Already the case in const contexts, just for all types that aren't trivially free of `Drop` types.
* Exactly the behaviour we want.
Extraneous `~const Trait` bounds where `Trait` isn't a bound on the type at all are still rejected:
```rust
impl<T: ~const Bar + ~const Baz> const Drop for Foo<T> {
fn drop(&mut self) {
self.0.thing();
}
}
```
errors with
```
error[E0367]: `Drop` impl requires `T: Baz` but the struct it is implemented for does not
--> src/lib.rs:13:22
|
13 | impl<T: ~const Bar + ~const Baz> const Drop for Foo<T> {
| ^^^^^^^^^^
|
note: the implementor must specify the same requirement
--> src/lib.rs:8:1
|
8 | struct Foo<T: Bar>(T);
| ^^^^^^^^^^^^^^^^^^
```
# Drawbacks
[drawbacks]: #drawbacks
## Adding any feature at all around constness
I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable code for compile time evaluation.
So the main drawback is that it creates interest in extending the system or add more effect systems, as we have now opened the door with an effect system that supports traits.
Even though I personally am interested in adding an effect for panic-freedom, I do not think that adding this const effect system should have any bearing on whether we'll add
a panic-freedom effect system or other effect systems in the future. This feature stands entirely on its own, and even if we came up with a general system for many effects that is (e.g. syntactically) better in the
presence of many effects, we'll want the syntax from this RFC as sugar for the very common and simple case.
## It's hard to make constness optional with `#[cfg]`
One cannot `#[cfg]` just the `const` keyword in `const Trait`, and even if we made it possible by sticking with `#[const_trait]` attributes, and also adding the equivalent for impls and functions,
`~const Trait` bounds cannot be made conditional with `#[cfg]`. The only real useful reason to have this is to support newer Rust versions with a cfg, and allow older Rust versions to compile
the traits, just without const support. This is surmountable with proc macros that either generate two versions or just generate a different version depending on the Rust version.
Since it's only necessary for a transition period while a crate wants to support both pre-const-trait Rust and
newer Rust versions, this doesn't seem too bad. With a MSRV bump the proc macro usage can be removed again.
## Can't have const methods and nonconst methods on the same trait
If a trait has methods that don't make sense for const contexts, but some that do, then right now it is required to split that
trait into a nonconst trait and a const trait and "merge" them by making one of them be a super trait of the other:
```rust
const trait Foo {
fn foo(&self);
}
trait Bar: Foo {
fn bar(&self);
}
impl const Foo for () {
fn foo(&self) {}
}
impl Bar for () {
fn bar(&self) {
println!("writing to terminal is not possible in const eval");
}
}
```
Such a split is not possible without a breaking change, so splitting may not be feasible in some cases.
Especially since we may later offer the ability to have const and nonconst methods on the same trait, then allowing
the traits to be merged again. That's churn we'd like to avoid.
Note that it may frequently be that such a trait should have been split even without constness being part of the picture.
# Alternatives
[alternatives]: #alternatives
## use `const Trait` bounds for conditionally-const, invent new syntax for always-const
It may seem tempting to use `const fn foo<T: const Trait>` to mean what in this RFC is `~const Trait`, and then add new syntax for bounds that allow using trait methods in const blocks.
Examples of possible always const syntax:
* `=const Trait`
* `const const Trait` (lol)
* `const(always) Trait` (`pub` like)
* `const<true> Trait` (effect generic like)
* `const! Trait`
## use `Trait<const>` or `Trait<bikeshed#effect: const>` instead of `const Trait`
To avoid new syntax before paths referring to traits, we could treat the constness as a generic parameter or an associated type.
While an associated type is very close to how the implementation works, neither `effect = const` nor `effect: const` are representing the logic correctly,
as `const` implies `~const`, but `~const` is nothing concrete, it's more like a generic parameter referring to the constness of the function.
Fully expanded one can think of
```rust
const fn foo<T: ~const Trait + const OtherTrait>(t: T) { ... }
```
to be like
```rust
const<const C: bool> fn foo<T>(t: T)
where
T: Trait + OtherTrait,
<T as Trait>::bikeshed#effect = const<C>,
<T as OtherTrait>::bikeshed#effect = const<true>,
{
...
}
```
Note that `const<true>` implies `const<false>` and thus also `for<C> const<C>`, just like `const Trait` implies `~const Trait`.
We do not know of any cases where such an explicit syntax would be useful (only makes sense if you can do math on the bool),
so a more reduced version could be
```rust
const fn foo<T>(t: T)
where
T: Trait + OtherTrait,
<T as Trait>::bikeshed#effect = ~const,
<T as OtherTrait>::bikeshed#effect = const,
{
...
}
```
or
```rust
const fn foo<T: Trait<bikeshed#effect = ~const> + OtherTrait<bikeshed#effect = const>>(t: T) { ... }
```
## Make all `const fn` arguments `~const Trait` by default and require an opt out `?const Trait`
We could default to making all `T: Trait` bounds be const if the function is called from a const context, and require a `T: ?const Trait` opt out
for when a trait bound is only used for its associated types and consts.
This requires a new `~const fn` syntax (sigils or syntax bikesheddable), as the existing `const fn` already has trait bounds that
do not require const trait impls even if used in const contexts.
An example from libstd today is [the impl block of Vec::new](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L406) which has an implicit `A: Allocator` bound from [the type definition](https://github.com/rust-lang/rust/blob/1ab85fbd7474e8ce84d5283548f21472860de3e2/library/alloc/src/vec/mod.rs#L397).
A full example how how things would look then
```rust
const trait Foo: Bar + ?const Baz {}
impl const Foo for () {}
const fn foo<T: Foo>() -> T {
// cannot call `Baz` methods
<T as Bar>::bar()
}
const _: () = foo();
```
This can be achieved across an edition by having some intermediate syntax like prepending `#[next_const]` attributes to all const fn that are using the new syntax, and having a migration lint that suggests adding it to every `const fn` that has trait bounds.
Then in the following edition, we can forbid the `#[next_const]` attribute and just make it the default.
The disadvantage of this is that by default, it creates stricter bounds than desired.
```rust
const fn foo<T: Foo>() {
T::ASSOC_CONST
}
```
compiles today, and allows all types that implement `Foo`, irrespective of the constness of the impl.
With the opt-out scheme that would still compile, but suddenly require callers to provide a const impl.
The safe default (and the one folks are used to for a few years now on stable), is that trait bounds just work, you just
can't call methods on them.
This is both useful in
* nudging function authors to using the minimal necessary bounds to get their function
body to compile and thus requiring as little as possible from their callers,
* ensuring our implementation is correct by default.
The implementation correctness argument is partially due to our history with `?const` (see https://github.com/rust-lang/rust/issues/83452 for where we got it wrong and thus decided to stop using opt-out), and partially with our history with `?` bounds not being great either (https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209). An opt-in is much easier to make sound and keep sound.
To get more capabilities, you add more syntax. Thus the opt-out approach was not taken.
## Per-method constness instead of per-trait
We could require trait authors to declare which methods can be const:
```rust
trait Default {
const fn default() -> Self;
}
```
This has two major advantages:
* you can now have const and non-const methods in your trait without requiring an opt-out
* you can add new methods with default bodies and don't have to worry about new kinds of breaking changes
The specific syntax given here may be confusing though, as it looks like the function is always const, but
implementations can use non-const impls and thus make the impl not usable for `T: ~const Trait` bounds.
Though this means that changing a non-const fn in the trait to a const fn is a breaking change, as the user may
have that previous-non-const fn as a non-const fn in the impl, causing the entire impl now to not be usable for
`T: ~const Trait` anymore.
See also: out of scope RTN notation in [Unresolved questions](#unresolved-questions)
## Per-method and per-trait constness together:
To get the advantages of the per-method constness alternative above, while avoiding the new kind of breaking change, we can require per-method and per-trait constness:
A mixed version of the above could be
```rust
const trait Foo {
const fn foo();
fn bar();
}
```
where you still need to annotate the trait, but also annotate the const methods.
But it makes it much harder/more confusing to add
```rust
trait Tr {
const C: u8 = Self::f();
const fn f() -> u8;
}
```
later, where even non-const traits can have const methods, that all impls must implement as a const fn.
# Prior art
[prior-art]: #prior-art
* I tried to get this accepted before under https://github.com/rust-lang/rfcs/pull/2632.
* While that moved to [FCP](https://github.com/rust-lang/rfcs/pull/2632#issuecomment-481395097), it had concerns raised.
* [T-lang discussed this](https://github.com/rust-lang/rfcs/pull/2632#issuecomment-567699174) and had the following open concerns:
* This design has far-reaching implications and we probably aren't going to be able to work them all out in advance. We probably need to start working through the implementation.
* This seems like a great fit for the "const eval" project group, and we should schedule a dedicated meeting to talk over the scope of such a group in more detail.
* Similarly, it would be worth scheduling a meeting to talk out this RFC in more detail and make sure the lang team is understanding it well.
* We feel comfortable going forward with experimentation on nightly even in advance of this RFC being accepted, as long as that experimentation is gated.
* All of the above have happened in some form, so I believe it's time to have the T-lang meeting again.
# Unresolved questions
[unresolved-questions]: #unresolved-questions
- What parts of the design do you expect to resolve through the RFC process before this gets merged?
* Whether to pick an alternative syntax (and which one in that case).
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization?
* We've already handled this since the last RFC, there are no more implementation concerns.
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC?
* This RFC's syntax is entirely unrelated to discussions on `async Trait`.
* `async Trait` can be written entirely in user code by creating a new trait `AsyncTrait`; there is no workaround for `const`.
* This RFC's syntax is entirely unrelated to discussions on effect syntax.
* If we get an effect system, it may be desirable to allow expressing const traits with the effect syntax, this design is forward compatible with that.
* If we get an effect system, we will still want this shorthand, just like we allow you to write:
* `T: Iterator<Item = U>` and don't require `where T: Iterator, <T as Iterator>::Item = U`.
* `T: Iterator<Item: Debug>` and don't require `where T: Iterator, <T as Iterator>::Item: Debug`.
* RTN for per-method bounds: `T: Trait<some_fn(..): ~const Fn(A, B) -> C>` could supplement this feature in the future.
* Alternatively `where <T as Trait>::some_fn(..): ~const` or `where <T as Trait>::some_fn \ {const}`.
* Very verbose (need to specify arguments and return type).
* Want short hand sugar anyway to make it trivial to change a normal function to a const function by just adding some minor annotations.
* Significantly would delay const trait stabilization (by years).
* Usually requires editing the trait anyway, so there's no "can constify impls without trait author opt in" silver bullet.
* New RTN-like per-method bounds: `T: Trait<some_fn(_): ~const>`.
* Unclear if soundly possible.
* Unclear if possible without incurring significant performance issues for all code (may need tracking new information for all functions out there).
* Still requires editing traits.
* Still want the `~const Trait` sugar anyway.
## Should we start out by allowing only const trait declarations and const trait impls
We do not need to immediately allow using methods on generic parameters of const fn, as a lot of const code is nongeneric.
The following example could be made to work with just const traits and const trait impls.
```rust
const fn foo() {
let a = [1, 2, 3];
let b = [1, 2, 4];
if a == b {}
}
```
Things like `Option::map` could not be made const without const trait bounds, as they need to actually call the generic `FnOnce` argument.
# Future possibilities
[future-possibilities]: #future-possibilities
## Migrate to `~const fn`
`const fn` and `const` items have slightly different meanings for `const`:
`const fn` can also be called at runtime just fine, while the others are always const
contexts and need to be evaluated by the const evaluator.
Additionally `const Trait` bounds have a third meaning (the same as `const Trait` in `impl const Trait for Type`):
They can be invoked at compile time, but also in `const fn`.
While all these meanings are subtly different, making their differences more obvious will not make them easier to understand.
All that changing to `~const fn` would achieve is that folk will add the sigil when told by the compiler, and complain about
having to type a sigil, when there is no meaning for `const fn` without a sigil.
While I see the allure from a language nerd perspective to give every meaning its own syntax, I believe it is much more practical to
just call all of these `const` and only separate the `~const Trait` bounds from `const Trait` bounds.
## `const fn()` pointers
Just like `const fn foo(x: impl ~const Trait) { x.method() }` and `const fn foo(x: &dyn ~const Trait) { x.method() }` we want to allow
`const fn foo(f: const fn()) { f() }`.
There is nothing design-wise blocking function pointers and calling them, they mainly require implementation work and extending the
compiler's internal type system representation of a function signature to include constness.
## `const` closures
Closures need explicit opt-in to be callable in const contexts.
You can already use closures in const contexts today to e.g. declare consts of function pointer type.
So what we additionally need is some syntax like `const || {}` to declare a closure that implements
`const Fn()`. See also [this tracking issue](https://github.com/rust-lang/project-const-traits/issues/10)
While it may seem tempting to just automatically implement `const Fn()` (or `~const Fn()`) where applicable,
it's not clear that this can be done, and there are definite situations where it can't be done.
As further experimentation is needed here, const closures are not part of this RFC.
## Allow impls to refine any trait's methods
We could allow writing `const fn` in impls without the trait opting into it.
This would not affect `T: Trait` bounds, but still allow non-generic calls.
This is simialar to other refinings in impls, as the function still satisfies everything from the trait.
Example: without adjusting `rand` for const trait support at all, users could write
```rust
struct CountingRng(u64);
impl RngCore for CountingRng {
const fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
const fn next_u64(&mut self) -> u64 {
self.0 += 1;
self.0
}
const fn fill_bytes(&mut self, dest: &mut [u8]) {
let mut left = dest;
while left.len() >= 8 {
let (l, r) = { left }.split_at_mut(8);
left = r;
let chunk: [u8; 8] = rng.next_u64().to_le_bytes();
l.copy_from_slice(&chunk);
}
let n = left.len();
let chunk: [u8; 8] = rng.next_u64().to_le_bytes();
left.copy_from_slice(&chunk[..n]);
}
const fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
Ok(self.fill_bytes(dest))
}
}
```
and use it in non-generic code.
---
# Discussion
## Attendance
- People: TC, nikomatsakis, tmandry, scottmcm, davidtwco, fee1-dead, Urgau, oli, Nadri, cramertj, yosh, eholk
## Meeting roles
- Minutes, driver: TC
## History
- 2025-01-15: [Const trait impls design meeting part 1](https://hackmd.io/rF-h-9e3StGZzKMMsSLwCw) -- This meeting.
- 2025-01-15: [const-trait-design-bikeshed](https://github.com/nikomatsakis/const-trait-design-bikeshed) -- Ideas and examples.
- 2025-01-15: [Design meeting discussion](https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-01-15) -- Discussion related to this meeting.
- 2025-01-13: [RFC 3762](https://github.com/rust-lang/rfcs/pull/3762) -- This RFC.
- 2025-01-08: [Reading notes on const trait impls draft RFC](https://hackmd.io/1Ff9ATdCRz-TU1YttCj03Q) -- Notes on and discussion of draft ahead of this meeting.
- 2024-11-27: [Nadri's effect elision](https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/Nadri's.20effect.20elision) -- Discussion of an elision proposal.
- 2024-11-25: [A proposal for effects in Rust](https://hackmd.io/InWtHvUFQnK5QRz_s5XCdA) -- A proposal draft by Oli.
- 2024-11-23: [Painting the shed before it's done](https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/painting.20the.20shed.20before.20it's.20done) -- Discussion ahead of the draft.
- 2024-11-23: [`const` and the revenge of the tilde sigil](https://hackmd.io/EAZ-rLglQMWU_pDWRDbITw) -- A proposal draft by Oli.
- 2024-10-21: [Why a predicate?](https://hackmd.io/STrJZtfGRrqjYYYVkLn7Gw) -- CE write-up on the implementation.
- 2024-08-09: [Project const traits issues](https://github.com/rust-lang/project-const-traits/issues) -- Issues working toward this draft and implementation.
- 2023-01-16: [Tracking Issue for removing impl const and ~const in the standard library](https://github.com/rust-lang/rust/issues/110395)
- 2020-01-02: [Tracking issue for const trait](https://github.com/rust-lang/rust/issues/67792)
- 2019-12-19: [const trait experiment](https://github.com/rust-lang/rfcs/pull/2632#issuecomment-567699174) -- This is where we approved an experiment for `const_trait`.
- 2019-02-05: [RFC 2632](https://github.com/rust-lang/rfcs/pull/2632) (not accepted) -- Oli's original RFC.
- 2019-01-13: [Meta tracking issue for const fn](https://github.com/rust-lang/rust/issues/57563).
- 2019-01-11: [const types, traits and implementations in Rust](https://varkor.github.io/blog/2019/01/11/const-types-traits-and-implementations-in-Rust.html) -- Varkor's proposal.
- 2018-10-05: [RFC for trait bounds on generic parameters of const fns](https://github.com/rust-lang/const-eval/pull/8) -- Discussion of draft leading to RFC 2632.
- 2018-07-21: [const fn and generics](https://github.com/rust-lang/const-eval/issues/1) -- A discussion started by RalfJ.
- 2018-05-20: [const_everywhere](https://github.com/Centril/rfcs/blob/rfc/const-everywhere/text/0000-const-everywhere.md) -- RFC draft by Centril.
- 2018-04-03: [Effects RFC notes](https://github.com/Centril/rfc-effects/issues) -- Centril's notes about an effect system.
- 2017-12-06: [RFC 2237](https://github.com/rust-lang/rfcs/pull/2237) -- Centril's RFC.
## Do we mark all methods with `const` / `~const`?
TC: There are two questions here, really:
- Do we want to mark the methods in the trait definition?
- Do we want to mark the methods in the impl?
## Splitting traits
scottmcm: +1 to saying that traits should be split if needed. I think that anywhere we *can't* do that today we should make lang changes to make it semver compatible (probably with some extra annotations or same-crate restrictions or something).
TC: I'm sympathetic to this, because I do think that we made a wrong turn somewhere in the design of traits like `Iterator` that have tons of methods (rather than having traits like `FlatMap` that could be impled on the appropriate things).
But still, asking people to break up traits for this particular reason feels like a workaround, and I wonder whether we might (or might not) feel differently about this for effects like `async` or `nopanic` (or `Send`), which is something else to consider.
nikomatsakis: +1 to both. I think one ought to be able to split traits. But I also want consistency across async and const and I feel like telling people they *must* split traits seems odd.
Points made during discussion:
* We have no real-world example of a "maybe const" trait where *some* methods should be const and *some methods* should not.
* Post-meeting, an example was given [here](https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-01-15/near/494029246).
* Adding `const` to trait or impl is not a breaking change; adding it to a fn in the body (to mean "perma-const" or something) would be.
* Maybe there is a correlation between traits where only some fns should be const and traits that should be split (disputed).
* Similarity to RTN in that an impl may want to refine a function item as const in a way that a caller could rely on.
(Discussion to the effect that, in this design, adding `const` / `~const` is never breaking.)
NM: But is that true? E.g.:
```rust
// rev 0
trait Foo {
type T: Default;
}
// rev 0 to 1a -- not a breaking change
const trait Foo {
type T: ~const Default;
}
// rev 0 to 1 -- not a breaking change
// rev 1a to 1 -- breaking change for users of the trait
const trait Foo {
type T: Default;
}
// rev 2
const trait Foo {
type T: ~const Default;
// ------ also a breaking change today to add this under the scheme described in this RFC, right?
}
```
(Post-meeting:)
TC: Yes, that would also be breaking, because we could have, e.g.:
```rust
#[const_trait]
trait Tr {
type Ty: Tr;
}
struct W<T>(T);
impl<T: Tr> const Tr for W<T> {
type Ty = T;
}
```
And then if we were to add:
```rust
#[const_trait]
trait Tr {
type Ty: ~const Tr; // <-- The diff.
}
struct W<T>(T);
impl<T: Tr> const Tr for W<T> {
type Ty = T;
//~^ ERROR the trait bound `T: ~const Tr` is not satisfied
}
```
## Where to put the sigil?
| option | not const | conditionally const | always const |
|-------------|----------------|---------------------|-----------------|
| RFC | `Trait` | `~const Trait` | `const Trait` |
| ~~old RFC~~ | `?const Trait` | `Trait` | unresolved |
| Josh | `Trait` | `const Trait` | `=const Trait` |
| Niko | `Trait` | `const Trait` | postpone \(\*\) |
errs: People can write this today:
```rust
const fn foo<T: Default>() -> T { }
```
NM: (I put some further thoughts below.)
yosh: Potentially silly option: This would be a lot easier if we could invert the polarity of `const`.
## `const fn` considered harmful?
nikomatsakis: I love the underlying semantics here but I don't think we've "nailed it" from a user-facing point-of-view. I see Nadri asking questions about mental model; I too have concerns. I think we should be thinking about this from the POV of "flavors" -- looking for consistency in the "feel" of how const and async are used.
One thing I note is that "maybe const" versus "always const" is a subtle thing, and I think creates a measure of confusion as implemented today. Punning on a single `const` keyword will also make it harder to have other forms of "maybe-ness" (e.g., maybe-async) should we opt to go that route (not saying we will, but I don't want to rule it out either).
There seem to be two areas where I think we could look for consistency across flavors `F`...
* How you indicate F-ness in functions and bounds.
* Here it seems surprising to me that the two uses of `const` are different in their meaning (`const fn<D: const Default>`).
* Nadri: how are they different?
* How you indicate F-ness in trait definitions and impls.
* Here I feel concerned because we tag individual functions as `async` but not individual functions as `const`.
* I realize that usage patterns are different and this is part of what leads us here, but I am concerned nonetheless and wonder if we can do better; I also expect eventually we will want traits that are "partly maybe const" and "partly never const" just as I think we want traits that "partly maybe async" and "partly never async", and it seems like the syntax should be analogous.
All of this got me wondering if we should revisit the decision to write `const fn`. Maybe it should be `const? fn foo()`, with `const?` used consistently for all "maybe const" things...
```rust
const? fn test<D: const? Default>(d: D) {
let foo = D::default();
}
const? trait Default { // <-- the trait may or may not be const
const? fn default(self) -> Self; // <-- if the trait is const, this function will be const (or not)
}
```
I personally find `const?` far more readable than `?const` but neither is *especially* easy on the eyes.
Nadri: If we compare with `async`, then a plain `fn` is a "maybe async" fn, since it can be run in both `fn` and `async fn` contexts. Comparson doesn't super work with `const`.
nikomatsakis: I don't agree. =) I mean, I know what you are saying is true, but I think it doesn't *feel* that way to users. Maybe that's what it is. But I'd like to see if we can do better.
TC (post-meeting): It's true, I think, only because we don't model `blocking` as an effect. If we did, then an `async` context wouldn't have the `blocking` effect and could not call plain `fn`s. This is why the claim feels false, because it really is. Even though we don't track this effect, it still exists, and users must track it themselves. You can't "really" call any `fn` in an async context -- just those that don't need the blocking effect.
Nadri: I see. here's one way this bothers me:
```rust
fn test<D: const Default>() {
let foo = const { D::default() };
}
const? impl Default for Foo { ... }
```
This `const?` impl allows me to call `test` on `Foo`, i.e. gives me a `Foo: const Default` bound (instead of `Foo: ?const Default`). That feels weird to me.
## `~const Destruct`
TC: Are we OK with this as a default supertrait? Might we want to make this a required thing thing to write to start, as we could always later then make it implicit (in the spirit of what we did for GAT required bounds)?
## Mental model of `~const`
Nadri: The discussion around `~const fn` makes me think there's a few ways to see the thing. This is me thinking out loud.
- `const fn` means "this body can run in both runtime and const contexts".
- In the world where `~const fn` means "body can run in either context", `const fn` would mean "body that can only run at compile-time"?
- TC: No. The two things, "requires runtime" and "requires compile-time" are two distinct effects, and are not inverses.
- Then I want `~const impl Trait for Foo` since this makes the `Trait` methods on `Foo` callable in either context.
- In `~const fn foo<T: ~const Trait>() {}`, are the two "maybe"s synchronized?
- `const fn foo<T: ~const Trait>() {}` means "in a const context, `T: Trait` must be const"
* Oli: fwiw: this is how non-const bounds also work kinda `const fn foo<T: Trait>() {}` is always const, but errors if the generic arg does not impl `Trait`. (Discussion topic; let's not chat about it here, just leaving the info for later.)
- `const fn foo<T: ~const Trait>() {}` could also be understood as "when `T: Trait` is const, then `foo` is too". In this sense `foo` is "conditionally const-callable".
* If we make the parallel with normal bounds, then when `T` does not impl `Trait`, `foo` is callable in no context at all (modulo trivial bounds). weird mental model I admit
## Note on marking methods
> Once we have some usage on stable, we evaluate whether people are looking for an opt-out and figure out how to add one. One solution is an attribute, considering that it's assumed to be rarely needed anyway.
David: If all methods are marked as const (per above section), then allowing people to do this could be as straightforward as saying you don't need to do that anymore and the non-const fns are the opt-out.
Oli: Yes, that's possible, but I think undesirable because it's super rare that it's needed, so even annotating with `const` at all is verbose, but also everywhere else we're saying "adding const to the function, trait or impl does not break things", so diverging from that by having const markers on methods, too, would be misleading.
## Iterator issues
cramertj:
In reference to:
> The closest thing we have is Iterator being hard to transform into a const trait in one go and we would like to just have a forever unstable option to slowly do it.
What is the specific concern with `Iterator` here? I don't see a mention of it in the `const Iterator` tracking issue: https://github.com/rust-lang/rust/issues/92476
fee1-dead: See https://github.com/rust-lang/project-const-traits/issues/15
cramertj: I see, this is the default trait method issue. It seems reasonable to imagine `Iterator` having default methods that are non-const and will never be `const` (or certainly, I can imagine someone writing a trait that wants this). It would be nice to allow particular methods to opt out of const-ness (this is discussed above already).
## `const Drop` instead of `const Destruct`
tmandry: The previous RFC 2632 described how we could do this without introducing a new trait, and making `const Drop` describe the whole drop glue. This is exactly what I wanted upon reading this RFC. (It also requires an edition to make `T: Drop` bounds consistent, but those are effectively useless today anyway.)
https://github.com/oli-obk/rfcs/blob/const_generic_const_fn_bounds/text/0000-const-generic-const-fn-bounds.md#const-drop-in-generic-code
nikomatsakis: +1, I am concerned about introducing a new concept, I would rather find some way (possibly via an edition) to rework `Drop` to mean "drop glue". That said, I also want to note the connection to [must move types](https://smallcultfollowing.com/babysteps/blog/2023/03/16/must-move-types/). A `T: Destruct` type is effectively "const must move". Interesting.
## Table
Options:
* Opt-out
* In a const fn (or a `const` trait method), all trait bounds are `~const` by default
* Outside a const fn, `T: const Foo` means always const
* You write `T: ?const` to opt-out
* Conditional-bound
* `T: ~const` means "const in a const context"
* `T: const` means "always const"
* Const-Conditional
* In a conditional const context, `T: const` means conditional context
* Otherwise it means always const
* Conditional-Fn
* Write `~const` everywhere that you mean "conditionally const"
* Runtime everywhere
* Write `runtime fn` and `T: runtime Trait`
* Runtime opt-out
* Write `runtime fn` and `T: Trait` inherits runtime
### Scenarios (all in current version)
Functions:
```rust
const fn foo<T: ~const Default>() { T::default() }
```
```rust
const fn foo<T: Default>() { }
```
```rust
fn foo<T: const Default>() { const { T::default() } }
```
```rust
struct Foo<T: Ord> {
t: T
}
const fn new<T: Ord>(value: T) -> Foo<T> {
// ------ this does not want to be `const`
Foo { t }
}
```
```rust
const trait Default {}
impl const Default for AlwaysType {}
impl Default for NeverType {}
impl<T> const Default for SometimesType<T>
where
T: ~const Default,
{}
```
### Opt-out
```rust
const fn foo<T: ?const Default>() { }
```
```rust
fn foo<T: const Default>() { const { T::default() } }
```
```rust
struct Foo<T: Ord> {
t: T
}
const fn new<T: ?const Ord>(value: T) -> Foo<T> {
Foo { t }
}
```
```rust
const trait Default {}
impl const Default for AlwaysType {}
impl Default for NeverType {}
impl<T> const Default for SometimesType<T>
where
T: Default,
{}
```
#### Conditional-bound
```rust
const fn foo<T: ~const Default>() { }
```
```rust
fn foo<T: const Default>() { const { T::default() } }
```
```rust
struct Foo<T: Ord> {
t: T
}
const fn new<T: Ord>(value: T) -> Foo<T> {
// ------ this does not want to be `const`
Foo { t }
}
```
```rust
const trait Default {}
impl const Default for AlwaysType {}
impl Default for NeverType {}
impl<T> const Default for SometimesType<T>
where
T: ~const Default,
{}
```
```rust
const fn foo<T: ~const Default>() { T::default() }
```
#### Const-Conditional
```rust
const fn foo<T: const Default>() { T::default() }
```
#### Conditional-Fn
```rust
~const fn foo<T: ~const Default>() { T::default() }
```
### Scenario: Const-able trait
TODO.
## Meaning of `const fn`
(Discussion about whether "const fn" is "maybe const" or "always const".)
TC: There are two ways to look at it (hopefully this is not too theoretical):
```rust
// `f` is a function with the empty set of effects.
fn f() do {};
// `f` is a function quantified over all effects.
fn f<effect K>() do K;
```
That is, is the function always total, or does the caller get to decide that?
This is described in much more detail here:
https://hackmd.io/1Ff9ATdCRz-TU1YttCj03Q#impl-const-Tr-for-
NM: The above analogy makes sense to me and is similar to how I was thinking about it.
Nadri:
```rust
const fn foo<T: ~const Default>()
// means
fn foo<effect K, T: Default<K>> do K()
```
This can be seen as:
1. "the context in which `foo` is called constrains the allowed `T`s"
2. or "the chosen `T` constrains the contexts in which `foo` can be called"
Option 2. makes me want `~const fn foo<T: ~const Default>()`, but then `~const` is only really useful for generic functions.
## Implied bounds notes
NM: We had talked about doing this:
```rust
struct Foo<T: Ord> { }
fn foo<T>(t: &Foo<T>) {
}
fn foo<T>(t: &Foo<T>, t: T) {
if t < t {
// it'd break me if you took the `T: Ord`
}
}
```
But that had the problem that `foo` could be broken by a far-off change to the type. So what we talked about doing instead is that you assume that the type in your arguments is well formed, but that if you want to make use of the bound yourself, you still have to explicitly write the bound. E.g.:
```rust
struct Foo<T: Ord> {
value: T
}
impl<T> Foo<T> {
pub fn new(value: T) -> Self {
Foo { value }
}
}
fn foo<T>(t: Foo<T>) -> Vec<Foo<T>> {
vec![t]
}
fn foo<T: Ord>(t: &Foo<T>, t: T) {
if t < t {
// it'd break me if you took the `T: Ord`
}
}
```
in this version, it's highly likely that `T: Ord` means you are *using* `Ord` in your function. Niko contends that presently this is not as true as it could be because often I am copying blocks of bounds around to make types satisfied but not to *use* them per se.
## Nadri arguing for "default to const"
Nadri: hypothesis: when I write a bound on a function, I intend to call one of its methods in the function context.
If this is true, then a user writing `const fn foo<T: Trait>()` likely expects a const constraint on `T: Trait`.
```rust!
const fn foo<T: Trait>() {
// intend to call a method
// => this should mean `T: const Trait`
}
```
There are other things we can do with a trait bound though: name a type, use a const, use a method somewhere else than the current context, or just satisfy a struct constraint
```rust!
// here no need for `T: const Default`
const fn foo<T: Default>() -> fn() -> T {
T::default
}
struct Foo<T: Ord> { ... }
// here also
const fn new<T: Ord>() -> Foo<T> { ... }
```
## Being more explicit...
TC: This isn't a proposal, exactly. But in an ideal world, I wish that we could do something very explicit first, and put that out in the world, and then add elision based on that learning and experience. E.g. (not a specific proposal):
```rust
trait Tr {
effect Kt; // Or, e.g., `do` rather than `effect`.
fn f() do Self::Kt; // Or, e.g., `\` or `/` rather than `do`.
}
impl Tr for () {
effect Kt = const;
fn f() do Self::Kt {}
}
fn maybe<effect K: ?const, T: Tr<Kt = K>>() do K
//~^ ~~~~~~ Required bound until we have `.do` syntax.
//~| Obviously we could come up with an attribute or some
//~| other way to spell this.
{
T::f()
}
const fn always<T: Tr<Kt = const>>() {
const { T::f() }
}
```
## Deltas
niko: Don't love `~const`.
tmandry: I keep wanting Conditional-Fn.
TC: I'd like to explore something similar to what we did with RTN. And, if possible, I'd prefer to start with an explicit effect syntax, like above, and then later add elisions on the basis of actual use.
## More discussion
For more analysis and discussion that happened somewhat ahead of this meeting, see:
- 2025-01-08: [Reading notes on const trait impls draft RFC](https://hackmd.io/1Ff9ATdCRz-TU1YttCj03Q)