C-like use of enum

i need to make something like this:

enum Attribute {
    MIGHT, DEXTERITY, INTELLIGENCE
}

int attributes[3] = {10, 10, 10};

void increase_attribute(Attribute attribute) {
    attributes[attribute] += 1;
}

with any other language i would use enum, however, as far as i can see, enum in rust is not suited to index an array. what should i use here then?

If your enum stores no data beyond the variant, you can cast it to an integer.

enum Attribute {
    Might = 0,
    Dexterity = 1,
    Intelligence = 2,
}

fn main() {
    println!("{}", Attribute::Dexterity as usize); // prints 1
}
4 Likes

In this example we have:

  • enum indexing (what this thread seems to be about),

  • and global, unsynchronized, mutable state.

1 - "enum indexing"

The idea is to use the enum discriminants as the indices of an array, c.f. your code above.

The thing is, non-assembly languages have a way to name offsets that does not requires these "hacks": structs.

typedef uint32_t Attribute;

typedef struct {
    Attribute might;
    Attribute dexterity;
    Attribute intelligence;
} AttributesNamed;

vs.

#define MIGHT 0
#define DEXTERITY 1
#define INTELLIGENCE 2
#define TOTAL 3

typedef Attribute AttributesIndexed[TOTAL];

Both AttributesNamed and AttributesIndexed have the same layout,
so the following union is a nice and useful way to express this property:

typedef union {
    AttributesIndexed indexed;
    AttributesNamed named;
} Attributes;

So that .named.might refers to the same place as .indexed[MIGHT], etc.

| .might        | .dexterity    | .intelligence |
| [0]           | [1]           | [2]           |

Now, the

#define MIGHT 0
#define DEXTERITY 1
#define INTELLIGENCE 2
#define TOTAL 3

is annoying to maintain and thus error-prone, so better let the compiler auto-increment these:

enum {
    MIGHT,
    DEXTERITY,
    INTELLIGENCE,

TOTAL};

Which, incidentally, let's us have a type that allows expressing at runtime / a dynamic indexed in a named manner, so let's name that type:

- enum {
+ typedef enum {
      ...
- TOTAL};
+ TOTAL} AttributeName;

and now we can use that enum to define our increment operation:

void increase_attribute (
    Attributes * attributes,
    AttributeName attr_name)
{
    Attribute * attr;
    switch (attr_name) {
        case MIGHT:
            attr = &attributes->named.might;
        break;
        case DEXTERITY:
            attr = &attributes->named.dexterity;
        break;
        case INTELLIGENCE:
            attr = &attributes->named.intelligence;
        break;
        default:
            __builtin_unreachable();
        break;
    }
    *attr += 1;
}

or, using the indexed version:

void increase_attribute (
    Attributes * attributes,
    AttributeName attr_name)
{
    Attribute * attr;
    switch (attr_name) {
        case MIGHT:
            attr = &attributes->indexed[(size_t) MIGHT];
        break;
        case DEXTERITY:
            attr = &attributes->indexed[(size_t) DEXTERITY];
        break;
        case INTELLIGENCE:
            attr = &attributes->indexed[(size_t) INTELLIGENCE];
        break;
        default:
            __builtin_unreachable();
        break;
    }
    *attr += 1;
}

which can be simplified to:

void increase_attribute (
    Attributes * attributes,
    AttributeName attr_name)
{
    size_t idx;
    switch (attr_name) {
        case MIGHT:
            idx = (size_t) MIGHT;
        break;
        case DEXTERITY:
            idx = (size_t) DEXTERITY;
        break;
        case INTELLIGENCE:
            idx = (size_t) INTELLIGENCE;
        break;
        default:
            __builtin_unreachable();
        break;
    }
    attributes->indexed[idx] += 1;
}

finally leading to your version:

void increase_attribute (
    Attributes * attributes,
    AttributeName attr_name)
{
    attributes->index[(size_t) attr_name]
}

Now, this may seem like overthinking this, but remember that we are no longer coding in assembly: we are coding in higher level languages that let us express the same properties as assembly, but in a more human-friendly fashion. Hence the comparison made above.

I'd have expected the named pattern to be a zero-cost abstraction (the whole point of these system programming languages), i.e., that all these function to compile down to the same operation (zero-extending the enum to a size_t, and then perform a raw indexing operation, even in the named cases), but it looks like the moment we are using Attribute * attr instead of size_t idx, the current compilers get confused for some reason, and do not generate optimal code. That's more of a bug within the compiler than an issue with the named patterns, though.

Rewrite-it in Rust

AttributeName

use AttributeName::*; #[allow(nonstandard_style)] // C-style: all-caps non-scoped variants
#[derive(Clone, Copy)]
#[repr(usize)]
pub
enum AttributeName {
    MIGHT,
    DEXTERITY,
    INTELLIGENCE,
}
const TOTAL: usize = INTELLIGENCE as usize + 1;

Attributes

use ::core::cell::Cell as Mut; // C-style mutation, see end of post as to why

type Attribute = u32;

#[derive(Debug)]
#[repr(C)] // to ensure ordering of fields.
pub
struct AttributesNamed {
    might: Mut<Attribute>,
    dexterity: Mut<Attribute>,
    intelligence: Mut<Attribute>,
}
type AttributesIndexed = [Mut<Attribute>; TOTAL];

#[repr(C)] // <- Paramount!
pub
union Attributes {
    named: AttributesNamed,
    indexed: AttributesIndexed,
}
// non-unsafe getters (Safety: one variant is valid iff the other one also is)
impl Attributes {
    #[inline]
    fn named (self: &'_ Self) -> &'_ AttributesNamed
    {
        unsafe { &self.named }
    }
    #[inline]
    fn indexed (self: &'_ Self) -> &'_ AttributesIndexed
    {
        unsafe { &self.indexed }
    }
}

increase_attribute

fn increase_attribute (
    attributes: &Attributes,
    attr_name: AttributeName,
)
{
    let attr: &Mut<Attribute> = match attr_name {
        | MIGHT => &attributes.named().might,
        | DEXTERITY => &attributes.named().dexterity,
        | INTELLIGENCE => &attributes.named().intelligence,
    };
    // *attr = *attr + 1
    attr.set(
        attr.get().wrapping_add(1)
    );
}
// or:
fn increase_attribute (
    attributes: &Attributes,
    attr_name: AttributeName,
)
{
    let attr: &Mut<Attribute> = &attributes.indexed()[attr_name as usize];
    // *attr = *attr + 1
    attr.set(
        attr.get().wrapping_add(1)
    );
}

2- Global, unsynchronized mutable state

This is unsafe within multi-threaded code, so Rust won't let use write that without unsafe, or without explicitely opting out of multi-threaded code, by making the "global" be thread local.

To be honest, the same problem arises in C, so I'll assume the global is actually a thread local one.

C

#ifndef __STDC_NO_THREADS__
# include <threads.h>
#endif

#ifndef THREAD_LOCAL
# if __STDC_VERSION__ >= 201112 && !defined __STDC_NO_THREADS__
#  define THREAD_LOCAL _Thread_local
// Portable thread_local annotation is a pain before C11
# elif defined _WIN32 && ( \
       defined _MSC_VER || \
       defined __ICL || \
       defined __DMC__ || \
       defined __BORLANDC__ )
#  define THREAD_LOCAL __declspec(thread) 
/* note that ICC (linux) and Clang are covered by __GNUC__ */
# elif defined __GNUC__ || \
       defined __SUNPRO_C || \
       defined __xlC__
#  define THREAD_LOCAL __thread
# else
#  error "Cannot define thread_local"
# endif
#endif
// Above was taken from: https://stackoverflow.com/a/18298965

THREAD_LOCAL
Attributes g_attributes = { .named = {
    .might = 10,
    .dexterity = 10,
    .intelligence = 10,
}};

void increase_global_attribute (AttributeName attr_name)
{
    Attributes * at_g_attributes = &g_attributes;
    increase_attribute(at_g_attributes, attr_name);
}

Rust

thread_local! {
    #[allow(nonstandard_style)] // C style
    static g_attributes = Attributes { named: AttributesNamed {
        might: 10.into(),
        dexterity: 10.into(),
        intelligence: 10.into(),
    }};
}

fn increase_global_attribute (AttributeName attr_name)
{
    g_attributes.with(|at_g_attributes: &Attributes| {
        increase_attribute(at_g_attributes, attr_name);
    });
}

Conclusion

In practice, I wouldn't recomment using a union and dynamically switching between both views, since it makes the code quite more complicated (I've only done that to compare multiple implementations, for learning purposes). Instead, I recommend picking a view (indexed or named), and sticking with it.

If you really really need to squeeze each nano-second of performance, the indexed view seems to lead to better assembly code, so it could be very marginally faster.

However, the named view leads to much more readable code, and is guaranteed to always be sound and panic-free :slightly_smiling_face:

3 Likes

I think that another alternative would be to use the enumn crate.

Why don't you just have an Attributes struct which has might, dexterity, and intelligence fields? It seems quite odd to use an array to store unrelated properties.

2 Likes

with struct it would be impossible to send a needed attribute as an argument to a function. i also came up with an idea to use a hashmap with string keys, because my attribute names will be stored in a database as strings, so i don’t need enum anymore.

In Rust, you can use a function pointer or a closure.

If you want to mess with the attribute directly in a struct, you can use a get_mut accessor method in addition to typical getters and setters.

Edit: I added a by_index method if you need to refer to a particular attr by number.

use Character::*;

fn main() {
    let mut attr = Attributes::new(10, 10, 10);
    let mut x = attr.might();
    x += 5;
    attr.set_might(x);

    mess_with_attr(attr.dexterity_mut());
    mess_with_attr(attr.by_index(3));

    dbg!(attr);
}

fn mess_with_attr(a: &mut u8) {
    *a = 1;
}

mod Character {
    #[derive(Debug)]
    pub struct Attributes {
        might: u8,
        dexterity: u8,
        intelligence: u8,
    }

    impl Attributes {
        pub fn new(might: u8, dexterity: u8, intelligence: u8) -> Attributes {
            Attributes {
                might,
                dexterity,
                intelligence,
            }
        }

        pub fn by_index(&mut self, index: u8) -> &mut u8 {
            match index{
             1 => &mut self.might,
             2 => &mut self.dexterity,
             3 => &mut self.intelligence,
             _ => panic!()
            }
        }
        
        pub fn might(&self) -> u8 {
            self.might
        }

        pub fn dexterity(&self) -> u8 {
            self.dexterity
        }

        pub fn intelligence(&self) -> u8 {
            self.intelligence
        }

        pub fn might_mut(&mut self) -> &mut u8 {
            &mut self.might
        }

        pub fn dexterity_mut(&mut self) -> &mut u8 {
            &mut self.dexterity
        }

        pub fn intelligence_mut(&mut self) -> &mut u8 {
            &mut self.intelligence
        }

        pub fn set_might(&mut self, value: u8) {
            self.might = value
        }

        pub fn set_dexterity(&mut self, value: u8) {
            self.might = value
        }

        pub fn set_intelligence(&mut self, value: u8) {
            self.might = value
        }
    }
}

You can have a struct and an enum, as showcased in my previous post.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.