cserializationenumsdeserializationx-macros

C enum implementing from_str and to_str functions? - Maybe an X-macro?


Given trivial typedef-free ANSI C code, how does one implement an X-macro to define everything in one place?

enum Foo {
    CAN, BAR, UNKNOWN
};

const char *Foo_to_str(enum Foo foo) {
    switch (foo) {
        case CAN:
            return "Can";
        case BAR:
            return "Bar is here";
        case UNKNOWN:
        /* `default:` like to keep this commented 
           out so compiler warns that case is missed */
            return "UNKNOWN";
    }
}


enum Foo Foo_from_str(const char *s) {
    if (strcmp(s, "Can")) return CAN;
    else if (strcmp(s, "Bar is here")) return BAR;
    else return UNKNOWN;
}

I started to mess around with arrays but not sure if I would be better served by a {{FOO, "Foo"}} style array, also I don't need the actual array so not sure if that's a good approach anyway:

#define FOOX \
  X(FOO, "Foo") \
  X(BAR, "Bar is here")

enum Foo{
#define X(_, N) N,
  FOOX
#undef X
};

char const *const FooA[] = {
#define X(N, _) #N,
    FooE
#undef X
};


const char* Foo_to_str(enum FooE foo) {
  return FooA[foo];
}

(would be great to separate interface and implementation in a header and source; but that might be a little ambitious considering macro usage)


Solution

  • #define X(N, _) #N Here you are making an array of the name parameter converted to a string, rather than returning the provided string. That is "BAR" rather than "Bar is here". Other than that the code looks mostly ok.

    I would recommend to use this style:

    #define FOO_LIST(X)       \
      /*name, str */          \
      X(FOO,  "Foo")          \
      X(BAR,  "Bar is here")
    

    Passing the X as macro parameter removes the need to define some mysterious X all over the place and later #undef it. Instead you can create situation-specific macros with unique names. Another advantage with this style is that if you have multiple X macro list data sets with the same parameters, you could reuse those situation-specific macros across multiple lists.

    A complete example would be something like this:

    #include <string.h>
    #include <stdio.h>
    
    #define FOO_LIST(X)       \
      /*name, str */          \
      X(FOO,  "Foo")          \
      X(BAR,  "Bar is here")
    
    enum Foo {
      #define FOO_ENUM(name, str) name,
      FOO_LIST(FOO_ENUM)
    
      FOO_N,          // number of items in the enum
      FOO_ERROR = -1  // some error code
    };
    
    const char* const FooA[] = {
      #define FOO_STR(name, str) str,
      FOO_LIST(FOO_STR)
    };
    
    const char* Foo_to_str(enum Foo f) {
      return FooA[f];
    }
    
    enum Foo Foo_from_str(const char* s) {
      /* 
        This is a naive search algorithm calling strcmp repeatedly in a
        nested list of ?: operators, ending with a -1 if nothing was found.
      */
      #define FOO_STRCMP(name, str) (strcmp(str,s)==0) ? name : 
      return FOO_LIST(FOO_STRCMP) FOO_ERROR;
    }
    
    int main()
    {
      puts(Foo_to_str(FOO));
      puts(Foo_to_str(BAR));
    
      printf("%d\n", Foo_from_str("Foo"));
      printf("%d\n", Foo_from_str("Bar is here"));
      printf("%d\n", Foo_from_str("Bad input"));
    }