Click here to Skip to main content
15,884,176 members
Articles / Programming Languages / C

A Smart Enum library in C (using X macros)

Rate me:
Please Sign up or sign in to vote.
4.94/5 (13 votes)
19 Aug 2016CPOL13 min read 19K   147   16   3
Following my first article about X macros, I describe how to build a macro library to extend the enum functionalities in C.

Introduction

In my first article, I explained how the X macros work and showed an example on how to turn enums into string. Everything in this article is based on X macros so I hardly recommend you to take a look at my article on the subject before reading it.

In fact, it was no coincidence if I choose this example. I'm currently using this macro library for my own projects.

In this article, I want to show how to create a more complete and useful X macro library to add functionalities to C enums.

As the subject is already partly covered, I will emphasize on how to add more functionalities on the basis of what is described on the X macros article.

You can download source of the library + an example main:

Download smartenum_example.zip - 2.4 KB

The entire project is also available on my GitHub.

Basic idea

As explained before, the starting point of this library was my need to have an efficient way to display enum values as string rather than numeric values.

Let say we deal with this enum:

C++
typedef enum IceCreamFlavor
{
    CHOCOLATE = 56,
    VANILLA = 27,
    PISTACHIO = 72,
} 
IceCreamFlavor;

Then, I want an automatically generated function that returns the name of a given enum element as string:

C++
// what I want
printf("%s", IceCreamFlavor_toString(CHOCOLATE));
// the printed result shall be CHOCOLATE (and not 56)

This is the basic idea to create a function for converting from enum value to string. But the macro library can be extended in order to generate many more functions !

Let's start to code

Basic tool

In my C project, as I tend to be close from OOP, I often name class function like this:

C++
classname_funcname

So I declare a macro $ like this:

C++
#define $(class, method) class##_##method

So I can declare and call class method like that:

C++
int $(MyStruct, getWidth)(MyStruct* me)
{
    if(me)
    {
        return me->width;
    }
    return 0;
}

MyStruct struct;
int width = $(MyStruct, getWidth)(&struct);

So, even for this library, I will use the macro $.

Naming convention

In order to avoid conflicts in macro names, I will prefix all the sub-macros by  __SMARTENUM.

The sub-macros that turns X macros entries into a line of code will be prefixed by __SMARTENUM_MACRO_ENTRY_TO.

The backbone

In this section, I will describe how I organize the code and what will be the final interface to use the library.

Unlike what I've done in the X macros article, I will separate the declarations from the definitions.

The library is working using X macro definition for the enum like that:

C++
#define SMARTENUM_IceCreamFlavor(_)\
    _(CHOCOLATE, 56)\ 
    _(VANILLA, 27)\ 
    _(PISTACHIO, 72)

Then, the two final macros will be:

C++
// to declare an enum with enum_name and the MACRO_DEFINITION entries
SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)
// to define the associated functions for the enum enum_name
SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)

An example of use:

C++
/* in .h file */
#define SMARTENUM_IceCreamFlavor(_)\
    _(CHOCOLATE, 56)\
    _(VANILLA, 27)\
    _(PISTACHIO, 72)

SMARTENUM_DECLARE(SMARTENUM_IceCreamFlavor, IceCreamFlavor)
// to generate both IceCreamFlavor enum and associated functions declarations

/*-----------*/

/* in .c file */
SMARTENUM_DEFINE(SMARTENUM_IceCreamFlavor, IceCreamFlavor)
// to generate associated functions definitions

The enum declaration

The first step for the library is to build the enum. So we want to build the macro that turn the macro definition of the enum into a proper C enum. This macro will be called by SMARTENUM_DECLARE.

First we need a macro to turn each X macro entry into an element line of the enum definition:

C++
#define __SMARTENUM_MACRO_ENTRY_TO_ENUM_ELEMENT(element_name, element_value)\
    element_name = element_value,

Then, we can create the macro that  build the enum:

C++
#define __SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
typedef enum enum_name\
{\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_ENUM_ELEMENT)\
}\
enum_name;

That is all we want to build our enum. If you are not convinced yet, please take a look at the X macro article.

We add it to the SMARTENUM_DECLARE macro:

C++
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)

Adding a toString function

As explained below, I will separate the declaration from the definition for the functions.

I want to add an automatically generated function, based on the macro definition of the enum, to turn an enum value into its name in string.

Let's have a look at what such a function can look if written by hand:

C++
const char* IceCreamFlavors_toString(IceCreamFlavors flavor)
{
    switch(flavor)
    {
    case CHOCOLATE: return "CHOCOLATE";
    case VANILLA: return "VANILLA";
    case PISTACHIO: return "PISTACHIO";
    default:
        // the error handling might seem a bit too strict !
        return 0;
    }
}

Declaration

For the declaration, we just need to use the enum_name parameter given to SMARTENUM_DECLARE.

We can then use this macro:

C++
#define __SMARTENUM_DECLARE_FUNCTION_TOSTRING(enum_name)\
const char* $(enum_name, toString)(enum_name value);

But, as I like to avoid to reuse the same code twice, I prefer:

1. to create a first macro __SMARTENUM_DECLARE_FUNCTION:

C++
// it just add semicolon at the end
#define __SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION, enum_name) __SMARTENUM_FUNCTION(enum_name);

2. create the macro __SMARTENUM_FUNCTION_TOSTRING:

C++
#define __SMARTENUM_FUNCTION_TOSTRING(enum_name)\
const char* $(enum_name, toString)(enum_name value)

3. then call this for declaration:

__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)

This doing, I can reuse __SMARTENUM_FUNCTION_TOSTRING for definition too. I ensure we are talking about the same thing in declaration and definition.

Definition

For the definition, the part we have the most to think about is how to turn an entry line of the X macro definition into a piece of code of this function.

In fact, this is pretty obvious here. With:

C++
// we need two parameters because this macro is working on X macro definition entries
// that have two parameters
#define __SMARTENUM_MACRO_ENTRY_TO_TOSTRING_CASE(element_name, element_value)\
    case element_name: return #element_name;

we turn:

C++
_(CHOCOLATE, 56)
//into
case CHOCOLATE: return "CHOCOLATE";

Then, we can declare the macro that build the function:

C++
#define __SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_TOSTRING(enum_name)\
{\
    switch(value)\
    {\
        MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_TOSTRING_CASE)\
        default: return 0;\
    }\
}

Filling the interface

Now we have both part, declaration and definition, let's implement it in the interface:

C++
// we add the declaration to SMARTENUM_DECLARE
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)

// and the definition to the SMARTENUM_DEFINE
#define SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\

And that's it !

We can test the library at this point:

C++
// in .h file
#define SMARTENUM_IceCreamFlavor(_)\
    _(CHOCOLATE, 56)\
    _(VANILLA, 27)\
    _(PISTACHIO, 72)

SMARTENUM_DECLARE(SMARTENUM_IceCreamFlavor, IceCreamFlavor)

// in .c file
SMARTENUM_DEFINE(SMARTENUM_IceCreamFlavor, IceCreamFlavor)

int main()
{
    IceCreamFlavor flavor = VANILLA;
    printf("%s\n", $(IceCreamFlavor, toString)(flavor)); // shall print VANILLA (if we are lucky !)
    return 0;
}

Now, I have introduced the way I structure the code. The way I will add new functionnality will always be the same:

  1. declaring the __SMARTENUM_FUNCTION for declaration and definition (the function signature)
  2. creating the __SMARTENUM_MACRO_ENTRY_TO macro that will turn each macro entry into code.
  3. creating the __SMARTENUM_DEFINE_FUNCTION using the macro definition and the __SMARTENUM_MACRO_ENTRY_TO macro.
  4. binding this small wolrd into my SMARTENUM_DECLARE and SMARTENUM_DEFINE final macros.

So, I will be quicker presenting the next functionalities. Now we will have more fun !

Let's add functionalities

The first functionality I want to add is related with the toString definition:

C++
#define __SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_TOSTRING(enum_name)\
{\
    switch(value)\
    {\
        MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_TOSTRING_CASE)\
        default: return 0;\
    }\
}

On the function, when the given value is not a valid enum element, the return is a NULL pointer. It can appear rude. But I want it like that.

But the rudest part is that we have no function that tell us what is a valid enum element. So let's add it !

Adding isValid function

declaration

We want a function that returns a boolean value and take an enum value as parameter. So the macro will be:

C++
#define __SMARTENUM_FUNCTION_ISVALID(enum_name)\
bool $(enum_name, isValid)(enum_name value)

definition

For the definition, I took inspiration from the toString function. We can switch over the enum value given and, for each macro definition entry, return true. Else, we return false as default.

So the __SMARTENUM_MACRO_ENTRY_TO macro will be:

#define __SMARTENUM_MACRO_ENTRY_TO_ISVALID_CASE(element_name, element_value)\
    case element_name: return true;

And the function definition will be almost the same as toString definition:

C++
#define __SMARTENUM_DEFINE_FUNCTION_ISVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_ISVALID(enum_name)\
{\
    switch(value)\
    {\
        MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_ISVALID_CASE)\
        default: return false;\
    }\
}

Filling the interface

Now we can fill the interface with our fresh macros:

C++
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)

#define SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)

And test:

C++
int main()
{
    IceCreamFlavor flavors[] = {(IceCreamFlavor)12, PISTACHIO};
    size_t number_of_flavors = sizeof(flavors)/sizeof(IceCreamFlavor);
    size_t i;
    
    for(i=0; i<number_of_flavors; ++i)
    {
        IceCreamFlavor flavor = flavors[i];
        
        if($(IceCreamFlavor, isValid)(flavor))
        {
            printf("%s\n", $(IceCreamFlavor, toString)(flavor), flavor);
        }
        else
        {
            printf("%d is not a valid IceCreamFlavor value !\n", flavor);
        }    
    }

    // shall print :
    // > 12 is not a valid IceCreamFlavor value !
    // > PISTACHIO

    return 0;
}

We have now secured the use of the toString function by adding a validity checking function.

Let's add some more functionnalities !

Adding fromString function

Now we have all the stuff to turn enum values into strings, we may need the opposite operation.

Let's write code to turn strings into enum values.

declaration

We want a function that returns an enum value and take string as parameter. So the macro will be:

C++
#define __SMARTENUM_FUNCTION_FROMSTRING(enum_name)\
enum_name $(enum_name, fromString)(const char* str_value)

definition

As usual, we have to build the macro that turns X macro definition entries into code. We just need to compare the given str_value to each enum element as string. If it match, then we return the enum element:

C++
#define __SMARTENUM_MACRO_ENTRY_TO_FROMSTRING_COMPARE(element_name, element_value)\
     if(!strcmp(#element_name, str_value)) return element_name;

Then, we include this generated code into the macro-built function:

C++
#define __SMARTENUM_DEFINE_FUNCTION_FROMSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_FROMSTRING(enum_name)\
{\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_FROMSTRING_COMPARE)\
    /* error handling : I return -1 as error value */ \
    return -1;\
}

If we expand this macro with our example enum, the built function will be:

C++
IceCreamFlavor IceCreamFlavor_fromString(const char* str_value)
{
    if(!strcmp("CHOCOLATE", str_value)) return CHOCOLATE;
    if(!strcmp("VANILLA", str_value)) return VANILLA;
    if(!strcmp("PISTACHIO", str_value)) return PISTACHIO;
    /* error handling : I return -1 as error value */
    return -1;
}
dealing with the returned error value

The function presented above was the first try. But I was not so happy with the error handling.

Returning -1 is consuming a valid enum entry. Then the statement:

C++
$(MyEnum, isValid)($(MyEnum, fromString)("UNKNOWN VALUE")) == false

With "UNKNOWN VALUE" a invalid entry is not always true.

If MyEnum is defined like that:

C++
#define SMARTENUM_MyEnum(_)\
    _(DOG, -1)\
    _(CAT, 0)\
    _(COW, 118)

Then $(MyEnum, fromString)("UNKNOWN VALUE") will return -1 that is the value of DOG which is a valid enum entry.

To avoid this unexpected behaviour, what I've done is to slightly modify the enum declaration macro:

C++
// I first declare a util macro to create a element name for unknown value
// I choose a complex name to reduce the chance that it is humanly declared
#define __SMARTENUM_MACRO_ENTRY_UNKNOWN_VALUE(enum_name) __##enum_name##_UNKNOWN_VALUE__

// Then, I add this element at the end of the enum declaration, 
// so I am sure that it is not consuming a value of the declaration
#define __SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
typedef enum enum_name\
{\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_ENUM_ELEMENT)\
    __SMARTENUM_MACRO_ENTRY_UNKNOWN_VALUE(enum_name)\
}\
enum_name;

And then, instead of returning -1 (which is quite arbitrary), I return the UNKNOWN_VALUE:

C++
#define __SMARTENUM_DEFINE_FUNCTION_FROMSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_FROMSTRING(enum_name)\
{\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_FROMSTRING_COMPARE)\
    return __SMARTENUM_MACRO_ENTRY_UNKNOWN_VALUE(enum_name);\
}

Then, calling $(MyEnum, fromString)("UNKNOWN VALUE") will now return 119 (__MyEnum_UNKNOWN_VALUE__) which is not a valid enum entry.

Filling the interface

Now we can fill the interface with our fresh macros:

C++
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_FROMSTRING, enum_name)

#define SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_FROMSTRING(MACRO_DEFINITION, enum_name)

And test:

C++
int main()
{
    const char* flavors_str[] = {"VANILLA", "SHAMPOO"};
    size_t number_of_flavors_str = sizeof(flavors_str)/sizeof(const char*);
    size_t j;
    
    for(j=0; j<number_of_flavors; ++j)
    {
        const char* flavor_str = flavors_str[j];
        
        IceCreamFlavor flavor = $(IceCreamFlavor, fromString)(flavor_str);
        
        if($(IceCreamFlavor, isValid)(flavor))
        {
            printf("%s is a valid IceCreamFlavor, its value is %d\n", flavor_str, flavor);
        }
        else
        {
            printf("%s is not a valid IceCreamFlavor !", flavor_str);
        }
    }

    // shall print :
    // > VANILLA is a valid IceCreamFlavor, its value is 27
    // > SHAMPOO is not a valid IceCreamFlavor !

    return 0;
}

We now have a function to turn strings into enum values. It can be useful if you have to retrieve an enum value from a configuration file.

But as for the toString function, to be perfectly safe, we need a function that checks if the given string is a valid enum entry.

Adding isStringValid function

declaration

We want a function that returns a boolean value and take an string as parameter. So the macro will be:

C++
#define __SMARTENUM_FUNCTION_ISSTRINGVALID(enum_name)\
bool $(enum_name, isStringValid)(const char* str_value)

definition

For the definition, I took inspiration from the fromString function. We still compare the given parameter to each enum element as string, but instead of returning the element value, we return true.

So the __SMARTENUM_MACRO_ENTRY_TO macro will be:

#define __SMARTENUM_MACRO_ENTRY_TO_ISSTRINGVALID_COMPARE(element_name, element_value)\
    if(!strcmp(#element_name, str_value)) return true;

And the function definition will be almost the same as fromString definition:

C++
#define __SMARTENUM_DEFINE_FUNCTION_ISSTRINGVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_ISSTRINGVALID(enum_name)\
{\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_ISSTRINGVALID_COMPARE)\
    return false;\
}

Filling the interface

Now we can fill the interface with our fresh macros:

C++
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISSTRINGVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_FROMSTRING, enum_name)

#define SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISSTRINGVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_FROMSTRING(MACRO_DEFINITION, enum_name)

And test:

C++
int main()
{
    const char* flavors_str[] = {"VANILLA", "SHAMPOO"};
    size_t number_of_flavors_str = sizeof(flavors_str)/sizeof(const char*);
    size_t j;
    
    for(j=0; j<number_of_flavors; ++j)
    {
        const char* flavor_str = flavors_str[j];
        
        if($(IceCreamFlavor, isStringValid)(flavor_str))
        {
            printf("%s is a valid IceCreamFlavor, its value is %d\n", flavor_str, $(IceCreamFlavor, fromString)(flavor_str));
        }
        else
        {
            printf("%s is not a valid IceCreamFlavor !", flavor_str);
        }
    }

    // shall print :
    // > VANILLA is a valid IceCreamFlavor, its value is 27
    // > SHAMPOO is not a valid IceCreamFlavor !

    return 0;
}

Now, we have a complete set of functions to serialize enum values.

Iterating through the enum

The next functions that I will show are related to enum iteration.

I have to admit that, in a first place, I had no idea on a real usage of enum iteration. I added these functions more for fun than real needs.

But thinking a bit more about the utility of iterating through enum made me find a valid example.

Let say that you have to provide a configuration file to the users of your app. In this configuration file, the users have to set a property that is an enum value. Then, you may want to inform the users on which values are valid to use. The file could looks like:

PowerShell
# flavor. accepted values are : CHOCOLATE, VANILLA, PISTACHIO
flavor=CHOCOLATE

Then, the iteration through the different values of your enum may appear useful when you automatically generate the comment on the configuration file. This way, each time you update the enum (adding or deleting values), the comment on the configuration file will always be updated too.

How to use

Before introducing the needed functions, I will start with some code to see how the iteration will work:

C++
size_t number_of_flavors = $(IceCreamFlavor, size)();
size_t i;

for(i=0; i<number_of_flavors; ++i)
{
    IceCreamFlavor flavor = $(IceCreamFlavor, at)(i);
}

So, as you can see, when I talk about iteration, it stays very simple. I am not introducing an iterator class. I just want a function that can return me the nth element of the enum, and another one that tell me the total number of elements of the enum.

Counting the number of elements

declaration

The function takes no argument and returns the size of the enum as size_t:

C++
#define __SMARTENUM_FUNCTION_SIZE(enum_name)\
size_t $(enum_name, size)()

definition

As my library is working on non-consecutive enums, I can't do something as simple as:

C++
typedef enum MyEnum
{
    ONE,
    TWO,
    THREE,

    NUMBER_OF_ELEMENTS
}
MyEnum;

printf("Number of elements of MyEnum : %d", NUMBER_OF_ELEMENTS);

But I still have my X macro definition to help me. Let see how we can use it to count the number of elements.

The idea is pretty simple: we just have to increment a variable for each X macro entry.

In a first version, I have done something like:

#define __SMARTENUM_MACRO_ENTRY_TO_SIZE_COUNT(element_name, element_value)\
    ++size;

Then for the full function:

C++
#define __SMARTENUM_DEFINE_FUNCTION_SIZE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_SIZE(enum_name)\
{\
    static size_t size = 0;\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_SIZE_COUNT);\
    return size;\
}

The problem with this, is that, each time the function is called, the number of elements is counted at runtime. I found a solution to avoid this by caching the result, but we still calculate the value at runtime at the first call.

The problem is that we have all the information at compile time, so we want that the compiler calculates it for us once for all.

So, here is a more intelligent version. Starting with the __SMARTENUM_MACRO_ENTRY_TO macro:

#define __SMARTENUM_MACRO_ENTRY_TO_SIZE_COUNT(element_name, element_value)\
    + 1

Then, the full function:

C++
#define __SMARTENUM_DEFINE_FUNCTION_SIZE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_SIZE(enum_name)\
{\
    static const size_t size = 0 MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_SIZE_COUNT);\
    return size;\
}

This way, we just turn each X macro entry into a + 1. Looking at the extended function for our example macro:

C++
size_t $(IceCreamFlavor, size)()
{
    static const size_t size = 0 + 1 + 1 + 1;
    return size;
}

The compiler will do the addition and we get rid of runtime calculation !

Filling the interface

Now we can fill the interface with our fresh macros:

C++
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISSTRINGVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_FROMSTRING, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_SIZE, enum_name)

#define SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISSTRINGVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_FROMSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_SIZE(MACRO_DEFINITION, enum_name)

And test:

C++
int main()
{
    printf("IceCreamFlavor enum has %d entries\n", $(IceCreamFlavor, size)());

    return 0;
}

Accessing elements by index

Now, we need to implement the function to access elements by index.

declaration

The function takes index as parameter and returns the corresponding enum value:

C++
#define __SMARTENUM_FUNCTION_AT(enum_name)\
enum_name $(enum_name, at)(size_t index)

definition

There are different possible implementations. If we think about previous implementations, we could implement it with a serie of if statements. It would not be very efficient, as we will need to check each previous value till reaching the good one.

For the implementation, I will use a static table of enum value, filled with all the values. Doing so, the enum values will be reachable by index.

First, we need to turn each X macro entry into a table entry:

C++
#define __SMARTENUM_MACRO_ENTRY_TO_TABLE_ENTRY(element_name, element_value)\
    element_name,

Then we can implement the entire function:

C++
#define __SMARTENUM_DEFINE_FUNCTION_AT(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_AT(enum_name)\
{\
    /* the static table with all enum values */ \
    static const enum_name indexed_values[] = \
    {\
        MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_TABLE_ENTRY)\
    };\
    /* the static size of the table */ \
    static const size_t sizeof_indexed_values = sizeof(indexed_values)/sizeof(enum_name);\
    if(index < sizeof_indexed_values)\
    {\
        return indexed_values[index];\
    }\
    /* we return the UNKNOWN_VALUE is the index is out of range */ \
    return __SMARTENUM_MACRO_ENTRY_UNKNOWN_VALUE(enum_name);\
}

Filling the interface

Now we can fill the interface with our fresh macros:

C++
#define SMARTENUM_DECLARE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_TOSTRING, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_ISSTRINGVALID, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_FROMSTRING, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_SIZE, enum_name)\
__SMARTENUM_DECLARE_FUNCTION(__SMARTENUM_FUNCTION_AT, enum_name)

#define SMARTENUM_DEFINE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_TOSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_ISSTRINGVALID(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_FROMSTRING(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_SIZE(MACRO_DEFINITION, enum_name)\
__SMARTENUM_DEFINE_FUNCTION_AT(MACRO_DEFINITION, enum_name)

And test:

C++
int main(int argc, char *argv[])
{
    size_t size_of_IceCreamFlavor = $(IceCreamFlavor, size)();
    size_t i;

    printf("There is %d entries in IceCreamFlavor:\n", size_of_IceCreamFlavor);
    
    for(i=0; i<size_of_IceCreamFlavor; ++i)
    {
        IceCreamFlavor flavor = $(IceCreamFlavor, at)(i);
        printf("\t- %s (%d)\n", $(IceCreamFlavor, toString)(flavor), flavor);
    }
    
    return 0;
}

Now, we have all that we need to iterate through the enum !

Final example use

Here under, I will present an example of use of this library, using every built functions:

C++
int main(int argc, char *argv[])
{
    char choice[100] = "";
    
    size_t size_of_IceCreamFlavor = $(IceCreamFlavor, size)();
    size_t i;
    bool flavor_chosen = false;
    IceCreamFlavor final_choice;
    
    printf("ICE CREAM SALER - Hi there, which flavor do you want ?\n");
    
    while(!flavor_chosen)
    {
        printf("ME - What do you have ?\n");
        printf("ICE CREAM SALER - I have %d different flavors:\n", size_of_IceCreamFlavor);
        for(i=0; i<size_of_IceCreamFlavor; ++i)
        {
            IceCreamFlavor flavor = $(IceCreamFlavor, at)(i);
            printf("\t- %s\n", $(IceCreamFlavor, toString)(flavor), flavor);
        }
        printf("ICE CREAM SALER - What do you want ?\n");
        printf("ME - ");
        scanf("%s", choice);
        
        if($(IceCreamFlavor, isStringValid)(choice))
        {
            final_choice = $(IceCreamFlavor, fromString)(choice);
            flavor_chosen = true;
        }
        else
        {
            printf("ICE CREAM SALER - Sorry man, I don't have %s !\n", choice);
        }
    }
    
    if($(IceCreamFlavor, isValid)(final_choice))
    {
        printf("ICE CREAM SALER - OK %s, great choice !\n",  $(IceCreamFlavor, toString)(final_choice));
    }
    else
    {
        printf("ICE CREAM SALER - Are you f*****g kidding ! I don't have this flavor !\n");
    }
    
    return 0;
}

Bonus

Now, just for fun, some more functions.

For those examples, I will be quick. I will just show you the definition part. I let you test them.

I have no real usage of those functions, but maybe someone can find it useful. If so, feel free to tell in the comments ;) !

Also, I don't pretend that the generated algorithms are optimal, even if I tried to do my best ! If you have better solutions, you can also add them in the comments !

First and last elements

We may need (or not) to get the first or the last element of the enum.

First element

What I do to return the first element is that for each X macro entry, I return the enum element. The first return will exit the function and every other return statements will be ignored !

The macro that turns each X macro entry into a return statement:

C++
#define __SMARTENUM_MACRO_ENTRY_TO_FIRST_RETURN(element_name, element_value)\
    return element_name;

Then the full function is:

C++
#define __SMARTENUM_DEFINE_FUNCTION_FIRST(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_FIRST(enum_name)\
{\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_FIRST_RETURN);\
}

Example of use:

C++
printf("first entry is %s", $(IceCreamFlavor, toString)( $(IceCreamFlavor, first)() ));

Last element

For returning the last element, what I do is that I assign the same variable with every entry of the X macro definition. The final value of the variable will be the last assignment !

C++
#define __SMARTENUM_MACRO_ENTRY_TO_LAST_ASSIGNMENT(element_name, element_value)\
    last = element_name;

Then the full function is:

C++
#define __SMARTENUM_DEFINE_FUNCTION_LAST(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_LAST(enum_name)\
{\
    static enum_name last;\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_LAST_ASSIGNMENT);\
    return last;\
}

Then, I hope that the compiler will reduce all those stupid assignements into the last one only :)

Else, we also could have use the at and the size functions to return the last element ! For this one implementation, I am still not really statisfied, so if you have a better solution, I take it !

Min and max elements

Still, I am not sure of the real utility of having functions to return the min and the max element of an enum (maybe if you are too lazy too find it by yourself ?). But anyway...

Min element

Just the same code that goes inside the loop of a normal min function:

C++
#define __SMARTENUM_MACRO_ENTRY_TO_MIN_COMPARE(element_name, element_value)\
    if(element_name < min) min = element_name;

Then we can build the full function:

Then the full function is:

C++
#define __SMARTENUM_DEFINE_FUNCTION_MIN(MACRO_DEFINITION, enum_name)\
__SMARTENUM_FUNCTION_MIN(enum_name)\
{\
    /* I use this boolean to cache the result */\
    static bool already_computed = false;\
    static enum_name min;\
    if(!already_computed)\
    {\
        /* initialization to the first element */\
        min = $(enum_name, first)();\
        MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_MIN_COMPARE)\
        already_computed = true;\
    }\
    return min;\
}

But I don't like the boolean trick to cache the result, because each time the function will be called, the boolean will be checked. To avoid this, I prefer to create a private function that do the calculation without caching the result, then I call this function at the static variable initialization:

#define __SMARTENUM_DEFINE_FUNCTION_MIN(MACRO_DEFINITION, enum_name)\
enum_name $(enum_name, min_private)()\
{\
    enum_name min = $(enum_name, first)();\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_MIN_COMPARE)\
    return min;\
}\
__SMARTENUM_FUNCTION_MIN(enum_name)\
{\
    static enum_name min = $(enum_name, min_private)();\
    return min;\
}

This way, the code of min_private will be executed only once, when the static variable is initialized. Then, the call of min function will just return the result without any caching consideration.

Max element

The code is quite the same:

C++
#define __SMARTENUM_MACRO_ENTRY_TO_MAX_COMPARE(element_name, element_value)\
    if(element_name > max) max = element_name;

And the full function is:

C++
#define __SMARTENUM_DEFINE_FUNCTION_MAX(MACRO_DEFINITION, enum_name)\
enum_name $(enum_name, max_private)()\
{\
    enum_name max = $(enum_name, first)();\
    MACRO_DEFINITION(__SMARTENUM_MACRO_ENTRY_TO_MAX_COMPARE)\
    return max;\
}\
__SMARTENUM_FUNCTION_MAX(enum_name)\
{\
    static enum_name max = $(enum_name, max_private)();\
    return max;\
}

And I will stop here !

Conclusion

I hope you enjoyed this article and you didn't find it too repetitive in regards to my first article about X macros.

My goal was to give you the complete library to enhance enums in C, to show in details the way I created it and to introduce different things that you can do with X macros.

The MACRO_DEFINITION can be seen as a foreach statement over the given entries. This statement takes a callback function as parameter to apply on the entries.

In the last section, I wanted to show how easy it is to add new functionalities. I stopped at min and max because the real utility is not obvious for me. But I could have add a mean function, a sort function, any kind of function that applies on list.

On this article, I wanted to go a little further in X macro use. I hope it will serve some developers in their future projects.

I you have any idea to improve the library, don't hesitate to add comment ;)

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
France France
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseThis is an amazing technichal achievement! Pin
Tomer Ussishkin31-Aug-22 23:12
Tomer Ussishkin31-Aug-22 23:12 
QuestionHow to pass a enum variable to Xmacro whcich should read the enum name and concat the enum name to another string Pin
ccodeworld.blogspot.com1-Jun-21 20:35
ccodeworld.blogspot.com1-Jun-21 20:35 
QuestionFound an enhancement for last, min and max function Pin
FredBienvenu19-Aug-16 0:48
FredBienvenu19-Aug-16 0:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.