3

[Update]

  1. the code given does actually work. I was mistaken when I thought that it did not. My bad; sorry++. If you can improve the code, please do so at https://codereview.stackexchange.com/questions/150480/generic-enum-to-text-lookup-in-c
  2. We must declare our strings at compile time. We code embedded systems and are not allowed to malloc(). Sorry for not being so clear.

[Update++] I will probably accept one of the answers below. I forgot to state, though, that our enums are non-contiguous and have a wide range, which can make a difference


The intertubes, and this site, are awash with questions asking for to get text from an enum.

I cannot find a canonical way to do this (and would accept one as answer to this question), so let's see if we can cobble one together between us.

Throughout our code, we have multiple arrays of structures, containing pairs of enums and corresponding strings.

The problem is that the strings have different lengths, so we code a lookup function for each one, which for loops over an array of structures, trying to match the enum and returning the corresponding text if a match is found.

Let’s take the following two contrived examples:

// +=+=+=+=+=+=+=+=+=+=+=+=+=+=
typedef enum
{
    north,
    south,
    east,
    west
} E_directions;

struct direction_datum
{
    E_directions direction;
    char         direction_name[6];
};

struct direction_datum direction_data[] =
{
    {north, "north"},
    {south, "south"},
    {east,  "east"},
    {west,  "west"},
};

// +=+=+=+=+=+=+=+=+=+=+=+=+=+=
typedef enum
{
    hearts,
    spades,
    diamonds,
    clubs,
} E_suits;

struct suit_datum
{
    E_suits suit;
    char    suit_name[9];
};

struct suit_datum suit_data[] =
{
    {hearts,   "hearts"},
    {spades,   "spades"},
    {diamonds, "diamonds",},
    {clubs,    "clubs"},
};

Apart from the string length, they are similar/identical, so, in theory, we should be able to code a generic function to loop over either direction_data or suit_data, given an index and return the corresponding text.

I am thinking something like this – but it doesn’t work (the enum value in the struct always seems to be zer0, so obviously my pointer arithmetic is off).

What am I doing wrongly?

char *Get_text_from_enum(int enum_value, 
                         void *array, 
                         unsigned int array_length, 
                         unsigned int size_of_array_entry)
{
    unsigned int i;
    unsigned int offset;

    for (i = 0; i < array_length; i++)
    {
        offset = i * size_of_array_entry;

        if ((int) * ((int *) (array+ offset)) == enum_value)
            return (char *)  (array + offset + sizeof(int));
    }

    return NULL;
}


printf("Expect south,    got %s\n", 
          Get_text_from_enum(south,    
                             direction_data, 
                             ARRAY_LENGTH(direction_data),  
                             sizeof(direction_data[0])));

printf("Expect diamonds, got %s\n", 
          Get_text_from_enum(diamonds, 
                             suit_data,      
                             ARRAY_LENGTH(suit_data),       
                             sizeof(suit_data[0])));
Community
  • 1
  • 1
Mawg says reinstate Monica
  • 38,334
  • 103
  • 306
  • 551

2 Answers2

5

There are two "canonical" ways to do this. One which is readable and one which avoids code repetition.


Readable way

The "readable way" is what I would recommend. It builds up an enum with a corresponding look-up table, where the enumeration constants match the look-up table indices:

typedef enum
{
    north,
    south,
    east,
    west,
    directions_n // only used to keep track of the amount of enum constants
} direction_t;

const char* STR_DIRECTION [] =  // let array size be based on number of items
{
  "north",
  "south",
  "east",
  "west"
};


#define ARRAY_ITEMS(array) (sizeof(array) / sizeof(*array))
...
// verify integrity of enum and look-up table both:
_Static_assert(directions_n == ARRAY_ITEMS(STR_DIRECTION), 
               "direction_t does not match STR_DIRECTION");

You can still have a struct based on this if you want:

typedef struct
{
  direction_t dir;
  const char* str;
} dir_struct_t;

const dir_struct_t DIRS [directions_n] = 
{ // use designated initializers to guarantee data integrity even if item order is changed:
  [north] = {north, STR_DIRECTION[north]},
  [south] = {south, STR_DIRECTION[south]},
  [east]  = {east,  STR_DIRECTION[east]},
  [west]  = {west,  STR_DIRECTION[west]}
};

No code-repetition way

The other alternative is to use so-called "X-macros", which is not really recommended other than as a last resort, since they tend make the code severely unreadable, particularly to those who aren't used at such macros.

This code is equivalent to my example above:

#define DIRECTION_LIST \
  X(north), \
  X(south), \
  X(east),  \
  X(west),         // trailing commma important here! (and ok in enums since C99)

typedef enum
{
  #define X(dir) dir
    DIRECTION_LIST
  #undef X
  directions_n // only used to keep track of the amount of enum constants
} direction_t;

typedef struct
{
  direction_t dir;
  const char* str;
} dir_struct_t;

const dir_struct_t DIRS [directions_n] = 
{
  #define X(dir) {dir, #dir}
    DIRECTION_LIST
  #undef X
};

This macro version gets rid of the explicit string look-up table.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 1
    You know, unless the code is a jumbled mess of inconsistent indentation and/or naming. I find "readability" to be highly subjective. – StoryTeller - Unslander Monica Dec 21 '16 at 10:16
  • 1
    @StoryTeller Of course. However, there is an industry consensus among the vast majority of C programmers saying that function-like macros reduce readability and is generally considered bad practice. – Lundin Dec 21 '16 at 10:17
  • I don't know. This particular controlled use of function-like macros doesn't feel as awful as the general case. But a good answer nonetheless. – StoryTeller - Unslander Monica Dec 21 '16 at 10:19
  • An excellent answer! I do like your "readable way", although I might still wrap it in a function, for bounds checking, etc. After several decades of C coding, I wasn't even aware of `designated initializers` :-( https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html Do I have your permission to base upon this for commerical porpoises? – Mawg says reinstate Monica Dec 21 '16 at 10:24
  • 2
    @Mawg The content posted here now belongs to SO and not to me. I think you are fine to use it, but I'm no expert on software licenses. There's probably some info over at Meta about that. – Lundin Dec 21 '16 at 10:28
  • 1
    ^^ [Here's one answer](http://meta.stackexchange.com/questions/12527/do-i-have-to-worry-about-copyright-issues-for-code-posted-on-stack-overflow) about copyright – StoryTeller - Unslander Monica Dec 21 '16 at 10:29
  • The "readable way" is the most elgenat and maintainable - but runs into problerms if I should ever have an invalid index. I do like it ***very*** much, though – Mawg says reinstate Monica Dec 22 '16 at 14:11
  • `[north] = {north, STR_DIRECTION[north]}` should probably be `[north] = {north, STR_DIRECTION[0]}`, it the idea is to work regardless of the ordering? – Lou Aug 15 '17 at 12:43
  • @Lou Indeed the idea is to not hard-code any numbers in the initializer list so that it is not accidentally broken during maintenance later on. Hence the comment "use designated initializers to guarantee data integrity even if item order is changed". – Lundin Aug 15 '17 at 12:54
  • Ok, I get it, the assumption is that `STR_DIRECTION` will also be rearranged and that enum values will always be sequential. I thought you were demonstrating a version where enums can get arbitrary values, but strings would be at fixed places. – Lou Aug 15 '17 at 12:57
  • @Lou No the enum must have values 0 to n but can otherwise be re-ordered. Otherwise the `directions_n` trick wouldn't work and the static assert would kick in. – Lundin Aug 15 '17 at 13:01
2

I always use the approach that is described below. Note that the data structure and the function are the same for all enums.

struct enum_datum  
{
    int    enum_val;
    char  *enum_name;
};

char *GetEnumName(enum_datum *table, int value)
{
   while (table->enum_name != NULL)
   {
      if (table->enum_val == value)
         return enum_name;
      table++;
   }
   return NULL;
}

After that for each specific enum you need to define:

typedef enum {
    north, south, east, west
} E_directions;

enum_datum E_directions_datum[] = 
{
    { north, "north" },
    { south, "south" },
    { east,  "east"  },
    { west,  "west"  },
    { some_value_not_important, NULL }, // The end of the array marker.
};

char *GetDirectionName(E_directions dir)
{
    return GetEnumName(E_directions_datum, dir);
}

Note that the string may not be exactly the same as the name of the enumerator. In my own projects sometimes I have several enum_datum arrays for the same enum. This allows getting more and less detailed messages without seriously complicating the overall design.

And pretty much that is it. The major advantage is simplicity.

Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51