rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Const self fields

Open izagawd opened this issue 1 month ago • 32 comments

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

izagawd avatar Nov 26 '25 19:11 izagawd

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.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.

CryZe avatar Nov 26 '25 20:11 CryZe

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.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.

izagawd avatar Nov 26 '25 20:11 izagawd

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 avatar Nov 26 '25 21:11 clarfonthey

@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.

izagawd avatar Nov 26 '25 22:11 izagawd

I have two questions:

  • How would this work with interior mutability? I.e. would i be able to store a Cell<u32> in a const self field?.
  • Can you take references to const self fields? (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 avatar Nov 26 '25 23:11 RustyYato

@RustyYato

How would this work with interior mutability? I.e. would i be able to store a Cell<u32> in a const self field?. Just like how rust deals with interior mutability in const today. 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

izagawd avatar Nov 26 '25 23:11 izagawd

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 avatar Nov 26 '25 23:11 RustyYato

@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.

izagawd avatar Nov 26 '25 23:11 izagawd

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
  • Place semantics
    • More intuitive with static-like syntax
    • Will give you a 'static place each time you refer to it, although it might be (de)duplicated at compile time
    • Doesn't allow interior mutability

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.

theemathas avatar Nov 27 '25 02:11 theemathas

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

izagawd avatar Nov 27 '25 03:11 izagawd

If anyone has any issues with how it works now, I am always willing to hear feedback.

izagawd avatar Nov 27 '25 19:11 izagawd

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 avatar Nov 28 '25 19:11 RustyYato

@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.

izagawd avatar Nov 28 '25 20:11 izagawd

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.

izagawd avatar Nov 28 '25 20:11 izagawd

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 avatar Nov 28 '25 20:11 RustyYato

@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.

izagawd avatar Nov 28 '25 21:11 izagawd

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?

bjorn3 avatar Nov 28 '25 22:11 bjorn3

@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.

theemathas avatar Nov 29 '25 00:11 theemathas

@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.

theemathas avatar Nov 29 '25 00:11 theemathas

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
}

programmerjake avatar Nov 29 '25 03:11 programmerjake

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.

bjorn3 avatar Nov 29 '25 08:11 bjorn3

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.)

RalfJung avatar Nov 29 '25 08:11 RalfJung

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 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, 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.

RalfJung avatar Nov 29 '25 08:11 RalfJung

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 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, 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.

@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

izagawd avatar Nov 29 '25 08:11 izagawd

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?

RalfJung avatar Nov 29 '25 08:11 RalfJung

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?

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.

izagawd avatar Nov 29 '25 08:11 izagawd

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.

izagawd avatar Nov 29 '25 09:11 izagawd

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 avatar Dec 04 '25 21:12 afetisov

@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.

izagawd avatar Dec 04 '25 22:12 izagawd

I accidentally closed the branch with a comment. My apologies

izagawd avatar Dec 04 '25 22:12 izagawd