Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic enum to text lookup in C

Tags:

c

enums

pointers

[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])));
like image 424
Mawg says reinstate Monica Avatar asked Sep 19 '25 12:09

Mawg says reinstate Monica


2 Answers

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.

like image 57
Lundin Avatar answered Sep 22 '25 18:09

Lundin


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.

like image 42
Kirill Kobelev Avatar answered Sep 22 '25 19:09

Kirill Kobelev