Indexing into x, y, z, w

#[repr(align(16), C)]
#[derive(Clone, Copy)]
pub struct Vec4f {
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub w: f32,
}

impl Vec4f {
  pub fn get(idx: usize): f32 {
    match idx => {
      0 => self.x,
      1 => self.y,
      2 => self.z,
      3 => self.w,
      _ => todo!(),
    }
  }
}

Is there a way to, without causing all types of undefined behaviour, to get rid of the match above? In a C like language, we could do something like:

((*f32) &self)[idx]
impl Vec4f {
    pub fn get(&self, idx: usize) -> f32 {
        // Safety: repr(C)
        let slice: &[f32; 4] = unsafe { std::mem::transmute(self) };
        // Alternative:
        //   `unsafe { &*(self as *const Vec4f as *const [f32; 4]) };`
        slice[idx]
    }
}

Though the verbosity is often preferred over unsafe.

You may want to implement Index and maybe IndexMut too.

1 Like

On second thought, am I solving the wrong problem here? Is the better solution to have native representatino be [f32; 4] then setup functions for .get_x, .set_x, .get_y, .set_y, ...

EDIT: for example:


#[repr(align(16), C)]
#[derive(Clone, Copy)]
pub struct Vec4f(pub [f32; 4]);

impl Vec4f {
    #[inline(always)]
    pub fn x(&self) -> f32 {
        self.0[0]
    }

    #[inline(always)]
    pub fn y(&self) -> f32 {
        self.0[1]
    }

    #[inline(always)]
    pub fn z(&self) -> f32 {
        self.0[2]
    }

    #[inline(always)]
    pub fn w(&self) -> f32 {
        self.0[3]
    }

    #[inline(always)]
    pub fn mx(&self) -> &mut f32 {
        &mut self.0[0]
    }

    #[inline(always)]
    pub fn my(&self) -> &mut f32 {
        &mut self.0[1]
    }

    #[inline(always)]
    pub fn mz(&self) -> &mut f32 {
        &mut self.0[2]
    }

    #[inline(always)]
    pub fn mw(&self) -> &mut f32 {
        &mut self.0[3]
    }

this seems cleaner

1 Like

Could be, although it's more common to just make a field public instead of having getters and setters for everything in Rust. (When dealing with references, getters and setters borrow the whole struct. Perhaps not a big concern here.) You'll get rid of some bounds checks.

Another thing I thought of but didn't suggest as I didn't think it would appeal (since it also doesn't use usize) is something like (rough sketch):

struct XIdx;
struct YIdx; // etc
impl Index<XIdx> for Vec4f { /* ... return self.x ... */ }
// ...
let x = v4[XIdx];

But it's pretty much the same idea other than getting to use the index operator.

4 Likes

In my experience, bytemuck tends to be a useful crate for doing these kinds of transmutations without using the unsafe keyword:

use bytemuck::{cast_mut, cast_ref};
use bytemuck::{Pod, Zeroable};
use std::ops::{Index, IndexMut};

#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C, align(16))]
pub struct Vec4f {
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub w: f32,
}

impl Index<usize> for Vec4f {
    type Output = f32;
    fn index(&self, idx: usize) -> &f32 {
        &cast_ref::<_, [_; 4]>(self)[idx]
    }
}
impl IndexMut<usize> for Vec4f {
    fn index_mut(&mut self, idx: usize) -> &mut f32 {
        &mut cast_mut::<_, [_; 4]>(self)[idx]
    }
}

fn main() {
    let mut v = Vec4f {
        x: 1.0,
        y: 2.0,
        z: 3.0,
        w: 4.0,
    };

    dbg!(v[0], v[1], v[2], v[3]);

    v[2] += 100.0;
    dbg!(v[0], v[1], v[2], v[3]);
}

(playground)

5 Likes

I implemented mine as follows. As long as the method is inlined, the temp array seems to be reliably optimized out.

    impl Index<usize> for Vec4 {
        type Output = f32;

        #[inline(always)]
        fn index(&self, i: usize) -> &f32 {
            [&self.x, &self.y, &self.z, &self.w][i]
        }
    }
6 Likes