Summary
Add associated "accessors" to traits, to allow access of member fields through trait objects without virtual function calls; plus enabling associated constants stored directly in the vtable.
(Edit: and also perhaps provide borrow splitting and as an realization of view types)
Motivation
Currently, to access data stored in a type via a trait object, you have to use a getter method, which incurs a virtual function call. This has extra overhead and inhibits optimization.
Same goes for associated constant items, they require adding a virtual call when ideally they can be stored directly in the vtable. (See also: Make associated consts object safe)
Design
We introduce a new type of associated items in traits: accessors. They can be seen as side-effect free methods that returns a constant value or a references to a member inside the type.
For example:
struct Earth {
mass: f32,
}
struct Moon {
mass: f32,
}
trait CelestialBody {
// Syntax pending, see below for alternatives
/// Mass in kilograms
fn mass(&self) -> &f32 = _;
// mutable accessor can exist too:
// fn mass(&mut self) -> &mut f32 = _;
}
impl CelestialBody for Earth {
fn mass(&self) -> &f32 = &self.mass;
}
impl CelestialBody for Moon {
fn mass(&self) -> &f32 = &self.mass;
}
// Using the accessor:
let x: &dyn CelestialBody = &Earth { mass: 1. };
x.mass(); // syntax pending
Inside the vtable, an offset into the implementing type is stored. And when accessor is used, the value is retrieved by derefencing at that offset. This guarantees that the implementing type is move safe.
More concretely:
#[repr(C)]
struct Vtable {
drop_in_place: unsafe fn(*mut ()),
size: usize,
align: usize,
mass_offset: isize,
}
// use
let x: &dyn CelestialBody = &Earth { mass: 1. };
*((x as *const _ as *const ()).byte_offset(vtable.mass_offset) as *const f32);
Similarly, constant associated items looks like this:
trait CelestialBody {
// &self signifies this must be used with a actual reference to an instance of
// CelestialBody.
const fn MASS(&self) -> f32 = _;
}
impl CelestialBody for Earth {
const fn MASS(&self) -> f32 = 5.972e24; // This must be a const experssion
}
// use
let x: &dyn CelestialBody = &Earth { mass: 1. };
x.MASS(); // 5.972e24
In this case, the actual value are stored directly inside the vtable. The value will be byte-copied when it's accessed.
Why not use the existing associated constant syntax
We could choose to make associated consts object safe, however, this approach has a difficult problem:
Constant associated accessors are fundamentally different from normal associated constants, in that they can only be accessed through actual reference to an instance of a given trait; whereas normal associated constant can be accessed through just a type name.
For example:
trait Trait {
const CONSTANT: u32;
}
// Use
fn use<T: Trait + ?Sized>() -> u32 {
<T as Trait>::CONSTANT
}
Since dyn Trait
implements Trait
, we should be able to use::<dyn Trait>()
. But this won't work.
If we introduce a different syntax for accessing associated consts via a trait object, then we create a sort of implicit type bound that can't be known without looking at the function body. And forcing the use of new syntax wherever T: ?Sized
bound exists would potentially be a breaking change.
So instead, we add a new kind of associated item, which can be accessed with a consistent syntax whether through a trait object, or through a concrete type - you must use a actual reference to value.
(this problem is pointed out by @steffahn in the discussion linked above)
Drawbacks
?
Alternative syntaxes
use
trait Trait {
use self::{field1: u32, mut field2: bool};
}
struct A(u32, bool);
impl Trait for A {
use self::{0 as field1, 1 as field2};
}
let
trait Trait {
let value: u32;
let mut value_mut: u32;
}
struct Type(u32);
impl Trait for Type {
let value = self.0;
}
// use
let concrete: &dyn Trait = &Type(10);
println!("{}", concrete.value); // 10
This syntax seems more natural, problem arises when we consider associated constants:
trait Trait {
// What should syntax for constant accessor be?
// const VALUE: u32 // this syntax already exists
// Proposal 1:
dyn const VALUE: u32;
// Proposal 2:
const VALUE: u32 where Self: ?Sized; // somewhat difficult to convey
// this can not be accessed like T::VALUE
}
Same as suggested but with a different keyword
Same as the examples in the design section, but replace fn
with something else.
Use an attribute
#[accessor]
fn mass(&self) -> f32;
// impl
#[accessor]
fn mass(&self) -> f32 { &self.mass }
Problem with this is, the user might think they are writing a normal function, and could be surprised by things that would work in a normal function not working here.
Bare
Like members in a struct definition:
trait Trait {
value: u32;
mut value_mut: u32;
}
This syntax just feels wrong.
Unresolved questions
- Is borrow splitting allowed?