Search code examples
rustlifetimeassociated-const

Assosiated function that returns an element of an assosiated const array?


I illustrate this general question with an example: I have a Color-Struct which holds three u8s (Red, Green, Blue). Color has an assosiated constant Color::PREDEFINED with some predefined colors. I need an assosiated function Color::pick_one returning one of these predefined colors, depending on a given parameter:

#[derive(Clone)]
struct Color (u8, u8, u8);
impl Color {
    const PREDEFINED: [Color; 3] = [
        Color(90, 250, 10),
        Color(120, 10, 10),
        Color(40, 10, 200)
    ];
    pub fn pick_one (param: i32) -> Color {
        let index = // do some math with `param`.
        Color::PREDEFINED[index]
    }
    pub fn to_string (&self) -> String {
        format!("rgb({}, {}, {})", self.0, self.1, self.2)
    }
}

Now this obviously doesn't work since pick_one returns a Color from PREDEFINED, but we cannot move out of type [Color; 3], a non-copy array.

A solution could be to clone the returned Color:

Color::PREDEFINED[index].clone()

but is this good performance?

I would also be fine with pick_one returning a &Color instead of a Color, but:

pub fn pick_one (param: i32) -> &Color {
    let index = // do some math with `param`.
    &Color::PREDEFINED[index]
}

gives:

missing lifetime specifier
this function's return type contains a borrowed value, but there is no value for it to be borrowed from
consider using the `'static` lifetime: `'static `

Now, is this the right place to use 'static?

I find it actually confusing that

pub fn pick_one (&self, param: i32) -> &Color {
    let index = // do some math with `param`.
    &Color::PREDEFINED[index]
}

works quite well – this looks a bit like the 3rd lifetime elision rule, right? But isn't the idea of this rule: A methos can't be called after self was dropped, thus, returned references to fields of self is always valid? But this isn't the point here, since PREDEFINED is not a field of self, but assosiated to Color itself, making references to PREDEFINED be valid as long as Color exists (i.e. always). And making pick_one a method is actually pointless, since always calling Color(0, 0, 0).pick_one(12) is technically possible, but doesn't make sense from a semantic point of view.

Now, what is the best way to implement such an assosiated function returning a value from an assosiated constant?


Solution

  • Is clone good for performance?

    Copying 3 bytes is extremely cheap. This will almost always be more performant than making a reference. This cheapness is why Copy exists, and you should use it here:

    #[derive(Clone, Copy)]
    struct Color (u8, u8, u8);
    
    pub fn pick_one (param: i32) -> Color {
        let index = ...;
        Color::PREDEFINED[index]
    }
    

    The general rule is to derive Copy whenever possible.

    Is this the right place to use 'static?

    Yes. If you're returning a reference, and its lifetime doesn't belong to one of the parameters, it's pretty much always 'static.

    pub fn pick_one (param: i32) -> &'static Color {
    

    pick_one (&self, param: i32) -> &Color

    This one only makes sense if you want to reserve the ability of producing the return from self in a future version without breaking your API. However, this also only makes sense for non-Copy types, so even if you produce the return from self, it should still be a straight Color with no reference.


    Side note: when you have a tuple of all the same type, consider doing struct Color([u8; 3]). This is equivalent in memory representation and gives you all the methods on array, helping to reduce code duplication, like:

    let c = Color([10, 10, 10]);
    let half_c = Color(c.0.map(|n| n / 2));