I thought about the fact that there isn’t a good way to talk about offsets in a struct (i.e., C++'s pointer-to-member) while putting together my unnamables post. Some prior discussion can be found here.
Currently, you can’t project fields through a custom reference type, like Pin. The workaround here is unsafe { ptr.map(|x| x.field) }, which requires unsafe, since the map function could move out of the Pin. To work around this problem, we introduce the type T.U, for T, U types. Then, we’d be able to write a function like this:
fn project<'b, U>(this: &'b mut Pin<'a, T>, field: T.U) -> Pin<'b, U> {
Pin { inner: this.inner.(field) }
}
// ..
struct Foo { x: i32 }
Pin::project(ptr, offsetof Foo.x)
Pin::project(ptr, offsetof _.x) // with type inference!
The type T.U is a field offset in the type T of type U, (i.e., an offset). This syntax is currently a parse error, so we can claim it for this use. We can use a field offset in place of a hardcoded identifier to access a field:
struct Foo { x: i32 }
let field: Foo.i32 = offsetof Foo.x; // take offset-of-field
let foo: Foo = ..;
foo.(field) // access the field pointed to by `field`
It is also acceptable to do a primitive cast to a usize, since this really is just an offset.
While Foo.x, would be symmetrical with the field access foo.x and the type name Foo.i32, whether Foo.x is “offset of” or “access field” is ambiguous relative to whether Foo is a type or a binding. I’m using the offsetof as a placeholder until we come up with a better syntax. Alternative syntax: <Foo>.x, though I don’t think the angle brackets are much better…
The .() operator is used for the same reason, since having foo.bar mean different things depending on whether a binding bar is in scope is a bad idea. Given that we need to use parens in a strange way to call a member fnptr, I think this is acceptable (see: (foo.f)(bar), since fields and methods are in different namespaces).
The syntax isn’t amazing, but it’s better than the alternatives I thought of. Analogy with C++'s T::*U would be confusing (not to mention that it’s an awful sigil) since users might expect a T::*mut U, even though mutability is meaningless for a field offset. T::&U is worse, since it insinuates there’s a lifetime involved. Granted, I think that since this is a fairly advanced feature (I have never encountered T::*U in C++ outside of a manual), it doesn’t need to be supremely ergonomic.
Edit: I wondered whether we could go further and overload field projection completely:
trait Project {
type Target<'a, T>; // requires GAT
fn project<T>(&'a self, offset: Self.T) -> Self::Target<'a, T>;
}
However, C++ does not allow for overloading operator., for good reason. Unless we come up with a better way to drop down to vanilla projection than unsafe { *(&foo as *const _).offset(offsetof Foo.x as isize) }, this is a bad idea. I think this is about as much of a good idea as overloading & (which C++ allows!)
We can probably do some clever things with the size of T.U, since structs very rarely have more than 255 fields. T.U will often be u8-sized, and, for structs with only one field of type U, a ZST. If there is no field of type U, T.U can be safely considered uninhabited! This allows for a rather silly function, to test if T has a field of type U:
const fn has_field_of<T, U>() { mem::size_of::<T.U>() != 0 }