2

I have been a C/C++ developer for more years than I care to remember. I am now starting out in Rust. Based on reading the documentation, I believe it will be able to address most, if not all, of my needs directly. However, I want to make sure that I can understand the Rust equivalent syntax for the following C++ syntax (I skipped error checking code for simplicity)

#include <iostream>
#include <map>

#define MY_ENUM \
  X(AA, 17) \
  X(BB, 42)

enum my_enum {
#define X(name, val) name = val,
  MY_ENUM
#undef X
};

std::string enum_to_str(my_enum x)
{
  static std::map<int, std::string> lookup = { 
#define X(name, val) {val, #name},
  MY_ENUM
#undef X
  };  
  return lookup.at(x);
}

my_enum str_to_enum(const std::string &x) 
{
  static std::map<std::string, my_enum> lookup = { 
#define X(name, val) {#name, my_enum(val)},
  MY_ENUM
#undef X
  };  
  return lookup.at(x);
}

int main()
{
  my_enum x = AA; 
  const std::string bb("BB");
  std::cout << "x=" << x << " which is " << enum_to_str(x) << std::endl;
  std::cout << bb << " is " << str_to_enum(bb) << std::endl;
}

I believe I can achieve the to_string() functionality using either the Display or Debug trait in Rust, like so.

#[derive(Debug)]
enum MyEnum {
    AA = 17, 
}

//use std::fmt;
//impl fmt::Display for MyEnum {
//    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
//       write!(f, "{:?}", self)
//    }
//}

fn main() {
    let x = MyEnum::AA;
    println!("x={:?}", x); 
}

What would be a way to do the string_to_enum() in Rust?

Paul Grinberg
  • 1,184
  • 14
  • 37
  • `serde` could be (ab)used to do it. That probably doesn't make sense in isolation, but I'm mentioning it in case your question is part of a bigger (de)serialization need. – Thomas Feb 12 '22 at 20:25
  • Also, see https://stackoverflow.com/a/39070533/14637 – Thomas Feb 12 '22 at 20:29
  • @Thomas - the `custom_derive, enum_derive` seems the most C-like straight forward Rust implementation of question. And to some extend I agree with @jonathan about X-Y problem statement, which is why I accepted his answer. Thank you both! – Paul Grinberg Feb 12 '22 at 22:13

1 Answers1

1

In rust, the normal way to convert strings into other types is by using the FromStr trait. However, FromStr can, by default, not be derived.

I thought I could recommend you the derive_more crate. Using this crate you can derive more traits, as the name implies. Unfortunately it does not support enums at this moment. I think that would be a fun addition to the library.

I think you can solve this problem with macros, but the clearest, cleanest, and most idiomatic way is just implementing FromStr yourself. You can use a match statement, matching on each of the strings and producing an enum variant. Doing this, you can also change the format of the strings which produce certain variants. For example, your enum variants might have capitalized names, but you may not want the strings to be capitalized as well. You could also add aliasses this way. An example:

enum MyEnum {
   A,
   B
}

struct FromStrError;

impl FromStr for MyEnum {
    type Err = FromStrError;

    fn from_str(v: &str) -> Result<Self, Self::Err> {
        match v {
            "a" => Ok(Self::A),
            "b" => Ok(Self::B),
            _ => Err(FromStrError),
        }
    }

}

fn main() {
    let parsed: MyEnum = "a".parse()?
}

I understand this may not be an answer to your exact question: You wanted something equialent to the X macro. I think this is the way you will want to implement this, but if you really prefer a macro solution please let me know. Your question feels to me a bit like an X-Y problem. "how can I do X in rust", while what you actually want may be Y. Anyway, let me know if I can help you further.

NOTE: the to_string functionality you definitely can achieve with derive_more! See here

EDIT: if you really want a macro solution, and I want to stress again that I don't think this is the right solution, you can do something like this:

// define a macro generating both FromStr and Display implementations
macro_rules! stringy_enum {
    (enum $name: ident {
        $($variant: ident = $value: literal),* $(,)?
    }) => {
        #[derive(Debug, PartialEq)] // for tests, no need if you don't want it
        enum $name {
            $($variant = $value),*
        }

        impl std::str::FromStr for $name {
            type Err = String;

            fn from_str(value: &str) -> Result<Self, Self::Err> {
                match value {
                    $(
                        stringify!($variant) => {Ok(Self::$variant)}
                    )*
                    _ => {
                        Err(format!("couldn't parse to {}", stringify!($name)))
                    }
                }
            }
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                match self {
                    $(
                        Self::$variant => {write!(f, stringify!($variant))}
                    )*
                }
            }
        }
    };
}


stringy_enum!(
    enum MyEnum {
        A = 3,
        B = 4,
    }
);

#[test]
fn example() {
    // strings can be parsed into enums
    assert_eq!(Ok(MyEnum::A), "A".parse());
    // enums can be converted into strings
    assert_eq!(MyEnum::A.to_string(), "A");
}

note that this solution does not accept custom derives on the enum (the macro doesn't parse those), it doesn't accept anything other than variant = name pairs, it does not accept a pub keyword or other visibility attribues (though that's easy to add), it does not accept any other custom attributes other than derive, it does not support variants with values, etc. This is just an example. Don't literally use it.

jonathan
  • 590
  • 3
  • 14
  • While `derive_more` cannot do that, there are plenty of other crates that can, e.g. [`strum`](https://lib.rs/crates/strum) ([`EnumString`](https://docs.rs/strum_macros/0.23.1/strum_macros/derive.EnumString.html)). – Chayim Friedman Feb 13 '22 at 03:41