-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: Const self fields #3888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
RFC: Const self fields #3888
Conversation
|
An alternative to this would be that we make existing associated constants object-safe. Currently, traits with Proposed Semantics:
Benefits over
|
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 |
|
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:
Note that these are mutually incompatible. Since that would amount to putting Cell in a global and allowing mutations to it. |
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
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 |
These are contradictory statements when interior mutability is involved. But I think this answers my question. Currently for consts you can only get a It should be possible to do that for |
|
@RustyYato ok this is news to me lmao. I have taken note of that. Yeah, I would have to add a |
text/3888-const-self-fields.md
Outdated
| `const self` declarations: | ||
| * Can only be assigned to constant expressions. | ||
| * Are per concrete type (for inherent impls) or per (Trait, ConcreteType) pair for trait implementations. | ||
| * The type `T` of a `const self` field must be `Sized`, `Freeze`, and `'static` to prevent unsoundness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this shouldn't require Freeze here, that should only be required to take a reference to a const self field.
It should be fine to use these fields by value, that's just like using a normal const by value.
|
I think the fundamental conflict here is whether this thing should behave like a value or a place.
And a big problem with the So here's an idea: a single attribute that can be applied to either a |
|
I think theemathas suggestion is nice, and it makes me think about having this syntax. Thoughts? Value only:
place:
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 |
Adjusted ambiguity rules a bit
|
If anyone has any issues with how it works now, I am always willing to hear feedback. |
text/3888-const-self-fields.md
Outdated
|
|
||
|
|
||
| `static const self` is similar to `const self`, however, working on `static const self` fields means working directly with its reference. Think: global `static` variable you are not allowed to mutate. | ||
| This means that the type of a `static const self` field must not have any interior mutability. In other words, the type of the field must implement `Freeze`. This is enforced by the compiler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
statics can't be generic. Allowing them in traits would incorrectly bypass this restriction given that trait impls can be generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I’m wrong, but my understanding is that Rust forbids generic statics because the compiler can’t guarantee a single, unique memory location for each instantiation. With monomorphization, the “same” generic static could end up duplicated in multiple codegen units, each with a different address
If that is the case, I did document that you cannot rely on static const self in having unique memory addresses.
Is that a bad design, or should I make it more clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a bad design. Part of being a static is having a single unique location in memory. That's why it is okay to allow them to be mutable (static mut or interior mutable static). We should definitely not allow anything to be mutable without a guarantee about its location in memory. And if we don't allow it to be mutable in any way, it can just be a const.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static might not have been the best name then, since static does imply a fixed memory location. Someone did recommend it to be called const self ref
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like it's just a constant of reference type, and doesn't have to be a separate language feature.
text/3888-const-self-fields.md
Outdated
| # Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| * Faster type matching than `dyn Any`: Since `dyn Any` does a virtual call to get the `TypeId`, using `static const self` to store the `TypeId` would be a much more efficient way to downcast. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be a bit more efficient. I don't think the perf difference would be all that much on modern cpus, especially when you also take the typeid comparison itself into account.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just did benchmarks. Since static const self is not actually a feature in rust, I simulated it by creating structs that were similar to it. These were the results:
dyn_typeid_eq: ~1.15 ns
static_const_self_typeid_eq: ~0.32 ns
the static const self equivalent seems to be more than triple times more performant than the dyn_typeid equivalent
Version details:
rustc 1.93.0-nightly (6647be936 2025-11-09)
binary: rustc
commit-hash: 6647be93640686a2a443a49f15c3390b68c8b5dd
commit-date: 2025-11-09
host: x86_64-pc-windows-msvc
release: 1.93.0-nightly
LLVM version: 21.1.3
My CPU is: AMD Ryzen 7 9800X3D if that helps
code:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a fair bit more than I would have expected.
|
I think that there isn't a need to split this feature into This should still allow you to take 'static references of your vtable impl. I think a |
|
@RustyYato The way I could see it work is: if the Also btw |
|
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 With trait objects, a That code is rejected because using |
|
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 |
|
I believe that this is what it is essentially doing Since working with if we did allow: let reference : &'static _ = &obj.CONST_FIELD;it would be surprising that using a |
|
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? |
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. |
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 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 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
} |
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.) |
FWIW, there have been prior discussions about dropping the requirement that |
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 if we have a collection of with |
Why would that have to be the case? |
Oh nvm. I kind of imagined assoc consts with trait objects as But still, dropping the requirement that dyn Trait implements Trait, I can't imagine how that would work. |
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. |
| struct Foo; | ||
|
|
||
| impl Foo { | ||
| const self CONST_FIELD: u32 = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it’s pointless to allow const self fields on inherent impls (except maybe for macros?), so I’d favor only allowing them on trait impls (unless someone can convince me otherwise).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only added them in inherent trait impls because usually, items available in traits are also available in inherent implementations. I am indifferent with this decision, so I can just remove it if most people think its unnecessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think keeping it for inherent impls for consistency would be nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reference, associated types are currently allowed in stable rust only in trait impls.
I don't see any reason to allow const self in inherent impls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reference, associated types are currently allowed in stable rust only in trait impls.
afaict that's mostly because of a limitation of the trait solver, rather than than Rust deciding we don't want associated types in inherent impls. it is available as a nightly feature, though iirc it's incomplete and buggy.
I don't see any reason to allow
const selfin inherent impls.
if you want to have things on your struct that act like fields (so you can do my_struct.some_field) but aren't actually stored in your struct, this is a good way (though it doesn't match the preferred name casing). abusing Deref also works in some cases.
|
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.
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. |
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
If you can think of a name that we can all agree would be better than
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 |
Given an fn foo(x: &dyn Trait) -> [u8; x.CONST] { ... }
Furthermore, these vtable fields are immutable at runtime, unlike typical fields of objects. So I agree, this has nothing to do with object fields. (Object fields are like struct fields in Rust.) |
Yeah, you are right. That's why I added "It would be similar to const methods in terms of validating its usability in const contexts." I probably should have been clearer. For instance, I don't see why we can't enable: const fn get_age_of(x: &dyn Trait) -> i32 { x.CONST_AGE } |
| } | ||
| ``` | ||
|
|
||
| This forces a dynamic function call, which is very slow compared to the `const self` and `const self ref` equivalent, and does not have as much compiler optimization potential. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be solved by:
- Having
constassociated methods in traits, ieconst fn value(&self) -> i32. - Special-casing the code-generation for const associated methods with no other arguments than
selfto returning a constant, rather than making a function call?
The reason I ask is because:
constassociated methods in traits are desirable on their own.- Once you have
constassociated methods in traits, optimizing the code-generation around them is also desirable on its own.
(Which doesn't necessarily mean that this RFC isn't worthwhile regardless)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Special-casing the code-generation for const associated methods with no other arguments than self to returning a constant, rather than making a function call?
How's that supposed to work if the function reads from interior mutable state in self (which a const fn totally can do)?
And even without interior mutability -- the vtable is fixed for the type, but the const fn could return some field of &self, thus depending on the concrete value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
Even a Freeze bound wouldn't be enough, as indirections would still allow reaching internal mutability.
A more dynamic approach is therefore necessary in the general case:
- A method is always emitted in the v-table, if only so a pointer to function can be taken.
- Additionally, if possible, the constant is also emitted. Perhaps directly in the v-table, perhaps storing a pointer in the v-table.
- Finally, a flag or offset is set in the v-table indicating whether the constant is available.
The runtime "call" can then be switched to:
- Check the flag/offset.
- If a constant is available, use the constant.
- Otherwise, fallback to calling the method (~25 cycles overhead).
As an optimization, if all implementations reachable from this call-site are known to always have the constant, the branch could be elided.
The cost of the branch should, in general, be fairly negligible, so performance will, in general, be close to that of a field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also wouldn’t be possible for the following reasons:
- It is possible for a
const fnto give different results between compile time and run time (see “NaN bit patterns”, and future considerations for stabilizingconst_eval_select). - If an implementation has the
const fnpanic or exhibit undefined behavior, then the program should panic or exhibit UB only when this particular function is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any problem here.
Since the promotion to a field is treated as an optimization, you've just enunciated some circumstances which prevent the optimization to take place... and that's all.
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.
Rendered