Access field of a generic type in function

Viewed 1277

I have spent the last 4 days trying to figure this one out and I'm really really stuck. Basically I have a large number of similar structs, all containing one particular field. let's call it data of type u32:

struct A {
    data: u32,
}

struct B {
    data: u32
}

...

struct N {
    data: u32
}

What I need to do is write a function outside those structs, which takes a generic type (that is, either one of those structs) perform some manipulation on the field and return the value. Basically something along the lines of:

fn some_manipulation<T>(st: &T) -> u32 {
    st.data * 10
}

Which, as it stands, is not possible because there is no data field on type T. Another thing is I cannot modify the structs. Is there a sensible way to achieve this?

3 Answers

So exactly as @Netwave pointed out, to make it work you need a trait that some_manipulation would use to access the data field, but to implement this trait for all the types you can use a custom macro.

Playground

I defined my macro to look like this impl_BorrowData!{u32, [A, B, N]} (where first argument is type of data field and then a list of structs follows, but macros are flexible enough and you can make it look as you wish.

trait BorrowData {
    type Output;
    fn borrow_data(&self) -> &Self::Output;
}

macro_rules! impl_BorrowData {
    ($out:ty, [$($t:ty),+]) => {
        $(impl BorrowData for $t {
            type Output = $out;
            fn borrow_data(&self) -> &Self::Output {
                &self.data
            }
        })*
    }
}

impl_BorrowData!{u32, [A, B, N]} // first the output type <u32>, then names of structs

fn some_manipulation<T: BorrowData<Output=u32>>(st: &T) -> u32 {
    st.borrow_data() * 10
}

fn main() {
    let a = A { data: 2 };
    println!("{}", some_manipulation(&a));
    let b = B { data: 50 };
    println!("{}", some_manipulation(&b));
}

The other answers explain how to use a trait to abstract over different types, and how to use macros to automate the process for fields of the same name in different types. There are two possible improvements (or, one addition and one improvement):

  • If there is only one data field that you need to borrow, you don't need to define a custom trait, you can use the AsRef trait from the standard library, and just implement it for your types.

  • To mutate the structs, which your question also asks about, your function must accept &mut T and your trait must return &mut u32. Because of the latter you cannot reuse the access trait for mutation, you have to have two traits, or always have the possibility of mutation. The standard library provides the AsMut trait as the mutating equivalent of AsRef.

Using the standard traits and allowing for mutation, the code would look like this (playground):

impl AsRef<u32> for A {
    fn as_ref(&self) -> &u32 {
        &self.data
    }
}

impl AsMut<u32> for A {
    fn as_mut(&mut self) -> &mut u32 {
        &mut self.data
    }
}

// and the same for B, or use a macro
...

fn some_manipulation<T: AsRef<u32>>(st: &T) -> u32 {
    *st.as_ref() * 10
}

fn mutating_manipulation<T: AsMut<u32>>(st: &mut T) {
    *st.as_mut() *= 10;
}

Rust generics are based around traits and trait bounds: a generic function can only access the features guaranteed by trait bounds. This is very much unlike e.g. C++ templates.

Here T has no bounds, so it can be (almost) anything and thus there is essentially no universally applicable operation (to say nothing of field access which is simply not an option in generic code, as fields are not a concept of traits).

There are two ways to perform this operation:

  1. if you really want to use a generic function, you need to define a trait defining the relevant operation and implement that trait for every structure, using a declarative macro is a common way to make "bulk" definition of trait impls less annoying

  2. alternatively, you could probably use a macro instead of a function, macros are more structural and will just fail to compile after macro expansion, though there are limit to what "declarative" macros can do, and "procedural" macros are not trivial

Related