user1002430
user1002430

Reputation:

Representing a limited-range integer with an enum in Rust?

I need an integral type that as a predefined limited range that includes 0, and want to implement like this:

#[repr(u8)]
pub enum X { A, B, C, D, E, F, G, H }

impl From<u8> for X {
    fn from(x: u8) -> X {
        unsafe { std::mem::transmute(x & 0b111) }
    }
}

When I need the integer value, I would cast with as u8. Arithmetic ops would be implemented by casting to u8 then converting back into the enum using from. And because I limit the range with the bitand when converting from u8 to the enum, I'm always in range of the enum.

Some benefits I can see are that the range is known to the compiler so it can skip bounds checking, and enum optimizations such as representing Option<X> as 1 byte.

A drawback I can see via assembly is that I incur and al, 7 every time I convert to enum, but I can live with that.

Is this a sound transmutation of u8 into the enum? What are other drawbacks of representing a limited range integer this way, if any?

Upvotes: 2

Views: 1151

Answers (1)

Peter Hall
Peter Hall

Reputation: 58805

I don't think there is anything wrong with this transmutation, in that it is likely sound. However, I believe it is unnecessary.

If performance is critical for your application, you should test on your target arch, but I used the Rust playground to show the generated ASM (for whatever arch the playground runs on):

Your version:

#[repr(u8)]
#[derive(Debug)]
pub enum X { A, B, C, D, E, F, G, H }

impl From<u8> for X {
    fn from(x: u8) -> X {
        unsafe { std::mem::transmute(x & 0b111) }
    }
}

#[no_mangle]
fn do_it_x(a: u8) -> X {
    a.into()
}

Explicit match:

#[repr(u8)]
#[derive(Debug)]
pub enum Y { A, B, C, D, E, F, G, H }

impl From<u8> for Y {
    fn from(y: u8) -> Y {
        match y & 0b111 {
            0 => Y::A,
            1 => Y::B,
            2 => Y::C,
            3 => Y::D,
            4 => Y::E,
            5 => Y::F,
            6 => Y::G,
            7 => Y::H,
            _ => unreachable!(),
        }
    }
}

#[no_mangle]
fn do_it_y(a: u8) -> Y {
    a.into()
}

The resulting assembly (from the playground at least) is:

do_it_x:
    pushq   %rax
    movb    %dil, %al
    movb    %al, 7(%rsp)
    movzbl  %al, %edi
    callq   <T as core::convert::Into<U>>::into
    movb    %al, 6(%rsp)
    movb    6(%rsp), %al
    popq    %rcx
    retq

do_it_y:
    pushq   %rax
    movb    %dil, %al
    movb    %al, 7(%rsp)
    movzbl  %al, %edi
    callq   <T as core::convert::Into<U>>::into
    movb    %al, 6(%rsp)
    movb    6(%rsp), %al
    popq    %rcx
    retq

Upvotes: 3

Related Questions