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 }