RFC: Const self fields
This RFC proposes const self fields: per-type constant metadata that can be accessed through values and trait objects using expr.FIELD syntax.
For trait objects, implementations store their constant data inline in the vtable, allowing &dyn Trait to read per-impl constants (like flags, versions) without a virtual function call. For non trait objects, it is as efficient as accessing a constant value.
This makes patterns like “methods that just return a literal” both more expressive and more efficient, especially in hot loops over many trait objects.
An alternative to this would be that we make existing associated constants object-safe.
Currently, traits with const items are not dyn-compatible (Error E0038). If we simply implemented the backend logic proposed in this RFC (storing constants in the vtable), we could lift this restriction for all associated constants.
Proposed Semantics:
- Continue using
const NAME: Type;in traits. - If the trait is used as a trait object, the compiler emits the constant into the vtable.
- Allow
obj.NAMEsyntax. Ifobjisdyn Trait, it performs a vtable lookup. Ifobjis concrete, it resolves to the static value (sugar forT::NAME). Though the latter isn't strictly necessary, just a nice to have for consistency.
Benefits over const self:
- Users don't have to choose between two kinds of constants definitions, they are just constants.
- Existing traits with constants immediately become dyn-compatible.
- Solves the motivation without expanding the language surface area.
An alternative to this would be that we make existing associated constants object-safe.
Currently, traits with
constitems are not dyn-compatible (ErrorE0038). If we simply implemented the backend logic proposed in this RFC (storing constants in the vtable), we could lift this restriction for all associated constants.Proposed Semantics:
* Continue using `const NAME: Type;` in traits. * If the trait is used as a trait object, the compiler emits the constant into the vtable. * Allow `obj.NAME` syntax. If `obj` is `dyn Trait`, it performs a vtable lookup. If `obj` is concrete, it resolves to the static value (sugar for `T::NAME`). Though the latter isn't strictly necessary, just a nice to have for consistency.Benefits over
const self:* Users don't have to choose between two kinds of constants definitions, they are just constants. * Existing traits with constants immediately become dyn-compatible. * Solves the motivation without expanding the language surface area.
I did consider going this route at first, but after deeper thought, I thought it was best that we needed something new.
let us assume a
trait Foo { const AGE: i32; }
Knowing how rust works, type dyn Foo implements Foo. That would mean that <dyn Foo>::AGE should be valid. But that can’t really work, because the bare type dyn Foo doesn’t have any metadata / vtable attached. We only get that once we have an actual trait object. It cannot know the value of AGE because the context given: <dyn Foo>::AGE, does not involve an underlying type.
FWIW, trait "fields" is a concept that has been proposed in the past but never had too much traction to get off the ground. I do think that effort on constant "fields" should probably work together with that.
@clarfonthey Trait fields and this RFC are very different though. This RFC is only about per-implementation constant metadata stored in the vtable, not per-instance data or layout/borrowing semantics.
Because we can’t currently add new entries to the vtable or change trait object metadata from a library, there’s no macro or workaround that can reproduce the performance benefits here (a direct metadata load instead of an indirect function call). I’m happy to frame this as a narrow “const metadata fields” subset that a future trait fields design could absorb, but it seems orthogonal enough that we don’t have to solve full trait fields first.
I have two questions:
- How would this work with interior mutability? I.e. would i be able to store a
Cell<u32>in aconst selffield?. - Can you take references to
const selffields? (I.e. a reference directly into the vtable)
Note that these are mutually incompatible. Since that would amount to putting Cell in a global and allowing mutations to it.
@RustyYato
How would this work with interior mutability? I.e. would i be able to store a
Cell<u32>in aconst selffield?. Just like how rust deals with interior mutability inconsttoday. try running this code
Just like how rust deals with attempted interior mutability mutations in const variables today. You can not mutate them.
Try this code
use std::sync::Mutex;
const MUTEX_NUM: Mutex<u32> = Mutex::new(0);
*MUTEX_NUM.lock().unwrap() += 5;
println!("{}", MUTEX_NUM.lock().unwrap());
It will still print out 0. You just can not mutate const variables, even if they have interior mutability.
since Cell<u32> can't be in const variables, (compiler won't let us do that due to thread stuff), I used Mutex<u32> instead .
So yes, you would be able to store something like a Mutex (not Cell. compiler stops you), but you can't mutate it.
Can you take references to const self fields? (I.e. a reference directly into the vtable)
Yes. As stated in the RFC, if you have
trait Foo {
const self FOO : i32;
}
You can get it's reference, and it will be considered a static reference
fn work_with_dyn_foo(input: &dyn Foo) {
let reference: &'static i32 = &input.FOO;
}
EDIT: Just realized the compiler does not stop you from putting Cell<T> in const variables, but it still doesnt let you mutate it anyways
Just like how rust deals with attempted interior mutability mutations in const variables today. You can not mutate them.
You can get it's reference, and it will be considered a static reference
These are contradictory statements when interior mutability is involved. But I think this answers my question.
Currently for consts you can only get a 'static reference when you have T: Freeze (no direct interior mutability). Otherwise what happens is you get a reference to a temporary. That's why your mutex example looks like no mutation is happening. And why you can get a 'static reference to a i32.
It should be possible to do that for const self fields. Could you add some text clarifying this to the RFC?
@RustyYato ok this is news to me lmao. I have taken note of that. Yeah, I would have to add a Freeze requirement. Thanks for letting me know.
I think the fundamental conflict here is whether this thing should behave like a value or a place.
- Value semantics
- More intuitive with
fn-like syntax - Will create a new "copy" of the value each time you use it, even for non-Copy types
- Allows interior mutability
- More intuitive with
- Place semantics
- More intuitive with
static-like syntax - Will give you a
'staticplace each time you refer to it, although it might be (de)duplicated at compile time - Doesn't allow interior mutability
- More intuitive with
And a big problem with the const-like syntax is that normal consts sometimes behave like a value and sometimes like a place, due to const promotion.
So here's an idea: a single attribute that can be applied to either a static or a fn. Let the user choose which semantics they want.
I think theemathas suggestion is nice, and it makes me think about having this syntax. Thoughts?
Value only:
const self X: Type = value
place:
static const self Y: OtherType = value
usage
let variable = obj.X; //ok. Copies it
let variable2 = &obj.X; // ok, but what it actually does is copy it, and uses the reference of the copy
let variable3 = obj.Y; // ok if the type of Y impls Copy
let variable4 = &obj.Y; // ok
If anyone has any issues with how it works now, I am always willing to hear feedback.
I think that there isn't a need to split this feature into static const self. Especially since normal statics are guaranteed to have a single address. I think just folding them together is better. Especially since we can specify that the reference behviour is the same as normal consts.
This should still allow you to take 'static references of your vtable impl.
I think a static self should only be introduced if it guarantees a single address for the value. Otherwise it introduces a footgun when combined with Mutex/RwLock/etc.
@RustyYato The way I could see it work is:
if the const self field's type implements Frozen, you can get a 'static reference without the fear of undefined behavior. If not, you can't and it will always work with a copy. This will indeed make it work similar to normal const variables. If this is what we go with, then right off the top of my head I do not see any issues with this. @theemathas What do you think?
Also btw static const self does enforce the field's type implements Frozen, so it won't even let you use Mutex/RwLock in the first place.
Oh wait, on second thought @RustyYato I remembered why I agreed with @theemathas about this.
In Rust, const is fundamentally “value substitution”: it means “copy the value and use it”, not “there is a unique storage location with this address”. In that sense, a const doesn’t conceptually have a memory address.
When we write:
const SOME_I32: i32 = 5;
let reference: &'static _ = &SOME_I32;
this is essentially behaving like:
let reference: &'static _ = &5;
The compiler decides to promote that temporary to a 'static allocation, but that’s because there would be no issues with it, not because that is what const variables are supposed to do if we go by its fundamental definition
With trait objects, a const self value isn’t known at compile time in the same way. The actual value depends on which concrete type’s vtable you end up with at runtime. We could model &obj.FIELD as a reference into vtable metadata, but that runs into the same kind of issues as in this example:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1b20ea1029f954495dcf6ba17df02048
That code is rejected because const is supposed to behave like “copy/substitute then use”, and allowing a 'static reference there would effectively promote a value with Drop into immortal global state (its destructor would never run). That breaks the mental model of const and is probably why the compiler is conservative about when it allows promotion to 'static.
using static const self, for instance, will enable us to store things that needs drop, and at the same time, enable us to get their 'static references.
If you wrap it in a const block, you can still get a 'static reference. https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=162f9612df52864a1e7a3a84ff781200
@RustyYato
I believe that this is what it is essentially doing
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=677ae063208965882b18ca507c8e2be8
Since working with const self means potentially working with trait objects, the const { &obj.FIELD } thing would probably not work, and if it did, would be weird since getting the reference is a runtime operation, unless we make some kind of macro to be able to get their references instead, which I am not so sure if its a good idea.
if we did allow:
let reference : &'static _ = &obj.CONST_FIELD;
it would be surprising that using a 'static lifetime for the reference of the const self field prevents its Drop implementation to be called.
Wouldn't a const static field as you specify it in this RFC be equivalent to having a const field that has a reference type?
@RustyYato
Especially since we can specify that the reference behviour is the same as normal consts.
We cannot.
The following code compiles in current rust:
const X: i32 = 1;
fn main() {
let a: &'static (i32, i32) = &(X, 2);
}
It is not feasible to get the exact same behavior with this RFC.
It might be possible to modify the rules of const promotion to limit what exactly is allowed with this RFC. However, const promotion is already extremely complicated, so that seems like a headache to me.
@bjorn3
Wouldn't a const static field as you specify it in this RFC be equivalent to having a const field that has a reference type?
I don't think so. A const field that's a reference would store a pointer in the vtable that points to the relevant value. A const static field would store the relevant value directly in the vtable.
maybe a good option is to use ref instead of static:
trait MyTrait {
const self A: Foo;
// ref means you get a reference when you access it, but the vtable still contains Bar directly
const self ref B: Bar;
fn f(&self);
}
fn demo(v: &dyn MyTrait, foo: fn(Foo), bar: fn(&'static Bar)) {
foo(v.A);
bar(v.B);
}
which is equivalent to (ignoring unsafe):
struct MyTraitVTable {
size: usize,
align: usize,
drop: fn(*mut ()),
A: Foo,
B: Bar,
f: fn(*const ()),
}
fn demo(v_data: *const (), v_vtable: &'static MyTraitVTable, foo: fn(Foo), bar: fn(&'static Bar)) {
foo(ptr::read(&v_vtable.A)); // copied (via ptr::read) just like normal `const`
bar(&v_vtable.B); // you get a reference since it was declared ref
}
I don't think so. A const field that's a reference would store a pointer in the vtable that points to the relevant value. A const static field would store the relevant value directly in the vtable.
That is semantically equivalent. The latter is merely an optimization over the former.
It's not even clear that it is an optimization. vtables can be duplicated, and having a copy of some large value in every one of them might not be great. We may want a pointer indirection anyway. (That also simplifies the implementation since it keeps all the vtable slots at pointer size.)
An alternative to this would be that we make existing associated constants object-safe.
I did consider going this route at first, but after deeper thought, I thought it was best that we needed something new.
let us assume a
trait Foo { const AGE: i32; }Knowing how rust works, type
dyn FooimplementsFoo. That would mean that<dyn Foo>::AGEshould be valid. But that can’t really work, because the bare typedyn Foodoesn’t have any metadata / vtable attached. We only get that once we have an actual trait object. It cannot know the value ofAGEbecause the context given:<dyn Foo>::AGE, does not involve an underlying type.
FWIW, there have been prior discussions about dropping the requirement that dyn Trait implements Trait. So this may still be a viable avenue. In terms of language feature economy, it seems nicer to reuse associated consts than to introduce an entirely new entity.
An alternative to this would be that we make existing associated constants object-safe.
I did consider going this route at first, but after deeper thought, I thought it was best that we needed something new. let us assume a
trait Foo { const AGE: i32; }Knowing how rust works, type
dyn FooimplementsFoo. That would mean that<dyn Foo>::AGEshould be valid. But that can’t really work, because the bare typedyn Foodoesn’t have any metadata / vtable attached. We only get that once we have an actual trait object. It cannot know the value ofAGEbecause the context given:<dyn Foo>::AGE, does not involve an underlying type.FWIW, there have been prior discussions about dropping the requirement that
dyn TraitimplementsTrait. So this may still be a viable avenue. In terms of language feature economy, it seems nicer to reuse associated consts than to introduce an entirely new entity.
@RalfJung
Associated consts and const self are different, I believe.
let us assume associated const with trait objects did exist, and we are going to use the trait Foo { const AGE: i32; } example
if we have a collection of Vec<Box<dyn Foo>>, they would have to all have the same AGE value
with const self, they can have varying ages
if we have a collection of
Vec<Box<dyn Foo>>, they would have to all have the same AGE value
Why would that have to be the case?
if we have a collection of
Vec<Box<dyn Foo>>, they would have to all have the same AGE valueWhy would that have to be the case?
Oh nvm. I kind of imagined assoc consts with trait objects as Box<dyn Foo<AGE = 5>>. kind of like how it is done with assoc types.
But still, dropping the requirement that dyn Trait implements Trait, I can't imagine how that would work.
It's not even clear that it is an optimization. vtables can be duplicated, and having a copy of some large value in every one of them might not be great. We may want a pointer indirection anyway. (That also simplifies the implementation since it keeps all the vtable slots at pointer size.)
Larger types would indeed lead to more binaries, but I do not see why it's a bad idea to give a developer more options to work with. Sometimes a developer might be willing to take that binary size increase to avoid the extra pointer chase.
While the feature seems useful, calling it "const" in any way is a misnormer. There is nothing "constant" about such fields. They are purely dynamic values stored in the vtable. You can't use them as real constants: you can't use them to compute const expressions, can't use them in static initializers, can't use them to define array sizes. I assume that the name comes from the desire to "dynify" associated constants, but the result isn't functionally or conceptually similar to constants in any way.
As of the day this RFC was published, there is no mainstream language with a similar feature.
I disagree. The feature is pretty close to typical fields of objects in OOP languages, bar language-specific in-memory representation details. E.g objects in C++ are quite similar to trait objects if the RFC is accepted: they contain (mutable or immutable) fields, interspersed in some compiler-specific way with pointers to virtual methods. This shows that the feature is useful, but also shows that there' s plenty of prior art to draw experience from.
@afetisov
There is nothing "constant" about such fields. They are purely dynamic values stored in the vtable. You can't use them as real constants: you can't use them to compute const expressions, can't use them in static initializers, can't use them to define array sizes.
You would be able to use them in constant expressions. I do not see why we can't, off the top of my head. It would be similar to const methods in terms of validating its usability in const contexts.
While the feature seems useful, calling it "const" in any way is a misnormer.
If you can think of a name that we can all agree would be better than const for this feature, then we could use that instead
I disagree. The feature is pretty close to typical fields of objects in OOP languages
I would have to disagree with your disagreement. The fields you are talking about are stored in the data of the object, not in the vtable/metadata. something like an interface in c# or java, do not have something like a field. Devs have to use getter methods, which still has the virtual call overhead.
In rust, trait objects currently do not have a concept of a "field" as well, so devs usually use trait object getter methods as a workaround too. But virtual method calls are much slower than something similar to a field access, and this RFC proposes field-like getters on a trait/interface, which no other language has.
I accidentally closed the branch with a comment. My apologies