Packing into a u16

Suppose we have a u14, then we can have something like:

pub enum State {
  Empty,
  Black(u14),
  White(u14)
}

In the absence of a u14, is there some way to build something similar that still fits in a u16 ?

EDIT:

// this doesn't work as u8 is too small
pub enum State {
  Empty,
  Black(u8),
  White(u8)
}

You can use an integer:

#[derive(Copy, Clone)]
pub enum State {
  Empty,
  Black(u16),
  White(u16)
}

#[derive(Copy, Clone)]
pub struct PackedState {
    value: u16,
}
impl PackedState {
    pub fn new(state: State) -> Self {
        let value = match state {
            State::Empty => 0,
            State::Black(i) => 1 | (i << 2),
            State::White(i) => 2 | (i << 2),
        };
        Self { value }
    }
    pub fn into_state(self) -> State {
        let kind = self.value & 0b11;
        let value = self.value >> 2;
        if kind == 0 {
            State::Empty
        } else if kind == 1 {
            State::Black(value)
        } else {
            State::White(value)
        }
    }
}

playground

1 Like

Use a newtype wrapper around u16 and write accessors…?

struct State(u16);

impl State {
    fn from_empty() -> Self {
        Sate(0x0000)
    }

    fn from_black(value: u16) -> Self {
        debug_assert!(value & 0xc000 == 0x0000);
        Sate(value | 0x4000)
    }

    fn from_white(value: u16) -> Self {
        debug_assert!(value & 0xc000 == 0x0000);
        Sate(value | 0x8000)
    }

    fn is_empty(&self) -> bool {
        self.0 & 0xc000 == 0x0000
    }

    fn is_black(&self) -> bool {
        self.0 & 0xc000 == 0x4000
    }

    fn is_white(self) -> bool {
        self.0 & 0xc000 == 0x8000
    }

    fn value(&self) -> u16 {
        debug_assert!(self.is_black() || self.is_white());
        self.0 & 0x3fff
    }
}
3 Likes

In general, the language cannot simply do this for you by (for example) allowing #[repr(packed)] on enums or adding uN types for all possible Ns because of alignment requirements. In particular, enums allow taking references to the fields inside them, and (unlike raw pointers) an unaligned &T is insta-UB. That means a u14 would still need to be layed out on two-word boundaries, and an enum containing u14s would still need some padding bits between them and the tag/discriminant. This is why uN types are mostly handled by crates; at the language level they end up being barely distinguishable from u16 and friends in practice.

So the only fully general answer to "how do you do this?" is "manually", because you have to throw out some of the features regular Rust enums provide to make your optimally packed layout work, and only you can decide which of those features your code can live without. The previous answers are good examples of how you might do this manually.

3 Likes