C and C++ were known for some time has having particularly underfeatured enumerations. Given the follow very temporary definition which will immediately breakdown when c so much as looks at it,

An ENUM: A set of discrete named values, and a type who’s stored value can be exactly one of those from the associated set.

It would also be nice for an enum type to be:

  • Printable: We can trivially get a printable string from any value of the enum.
  • Iterable: We can iterate through the values in the set represented by the enum.
  • Indexable: If there’s data associated with the various enum values, we can use the enum to refer to, or look up, the piece of data associated with one of it’s values.
  • Closed: We have some sort of facilities or language features to prevent values of enum type from being assigned arbitrary numeric values that have no meaning in the enum.
  • Usable as Sentinel: A value of enum type can be used to indicate which procedure or data should be used.

But wait! We can’t begin talking about sentinels without addressing the ginormous …


RUST

That is in the room, …

Alright.

If you’re not aware, Rust has built out it’s implementation of the enum type family into also being an implementation of discriminated unions. Which is FIRE, and I suspect one of the best language design decisions of its decade. So fire in fact, that regardless of who did this first, many languages are picking up a syntax for such a purpose. See std::variant in c++.

Unfortunately, I am here blogging about c-land, and we will not be having anything of the kind in c-land for a long while, if ever. We might finally get defer in c29. I’ll have to write about defer sometime…

I kid. I was aware of c’s stability and slow feature cycle when I picked it up. But that is to say that this article is about solving the much simpler problem of making nicely printable and slightly less error prone enums for c. For what it’s worth, we will talk about sentinels and discriminated unions at the end.

Which reminds me, back to


C

Here’s a basic enum declaration in c.

enum ShapeKind
{
    Dot,
    Line,
    Circle,
    Square,
};
enum ShapeKind shape = Circle;

Wait, hold on, let’s add some typedefs…

typedef enum ShapeKind
{
    Dot,
    Line,
    Circle,
    Square,
} ShapeKind;
ShapeKind shape = Circle;

Okay there we go. And now, let’s examine the various things you can do with it at varying levels of irresponsibility. Can we assign an arbitary value to a value of type ShapeKind?

// clang approved ✅
shape = 0x10101010;

Sure can! That kind of immediately defeats the idea of a type that can have one of a closed set of enumerated possibities. Bummer.

We can also define the enum members as having specific, non-sequential values. i.e.

typedef enum ShapeKind
{
    Dot = 0,
    Line = 5,
    Circle = -8,
    Square = 0x10101010,
} ShapeKind;

Additionally, the lack of member syntax means that instead of other languages’ shape = ShapeKind.Line or the abbreviated shape = .Line, c gives you no syntactical association between an enum’s members and it’s typename.

For this reason, in c we often accept the more clear but also disappointingly verbose

typedef enum ShapeKind
{
    ShapeKind_Dot,
    ShapeKind_Line,
    ShapeKind_Circle,
    ShapeKind_Square,
} ShapeKind;

Despite being less sparkling, this is the same amount of typing as member syntax ShapeKind.Line. Perhaps not as error prone in this form, but certainly still not closed. Oh well. Can we make it iterable?

By technicality it is, you could manually count the number of entries in the ShapeKind enum to iterate from 0 to that count. But realistically you should just do this:

typedef enum ShapeKind
{
    ShapeKind_Dot,
    ShapeKind_Line,
    ShapeKind_Circle,
    ShapeKind_Square,
    ShapeKind_Count,
} ShapeKind;

// Now you can trivially write
for (int i = 0; i < ShapeKind_Count; i++)
{
    // You know, as long as you didn't assign disjoint values to anything 
    //  in the enum and no one ever adds any members after ShapeKind_Count
}

Alright, now it’s iterable. Hey, let’s recheck our list:

  • ❔ Printable
  • ✅ Iterable
  • ❔ Indexable
  • ❌ Closed
  • ❔ Usable as Sentinel

Printable and Indexable go together, becuase if the enum is indexable, we can use it to get references to label strings. Here’s a naive printable implementation:

typedef enum ShapeKind

    ShapeKind_Dot,
    ShapeKind_Line,
    ShapeKind_Circle,
    ShapeKind_Square,
    ShapeKind_Count,
} ShapeKind;

char *shapeLabels[] = 
{
    "ShapeKind_Dot",
    "ShapeKind_Line",
    "ShapeKind_Circle",
    "ShapeKind_Square",
    "ShapeKind_Count",
};

print("name of shape: %s\n", shapeLabels[ShapeKind_Dot]);
// prints:
//  name of shape: ShapeKind_Dot

I’m not willing to call this printable, here’s why.

We now have two authoritative disconnected objects (the enum, and the array) that store associated data that must agree but can be changed to disagree. And we won’t get a compiler warning if they fall out of sync.

But we were not the first to encounter this problem so a, respectfully, somewhat tortured solution exists. It uses an additional file shape_enum_contents.h, which we’ll take a closer look at shortly.

typedef enum ShapeKind
{
    #define ENUM_ENTRY(item) JOIN(ShapeKind_, item) ,
    #include "shape_enum_contents.h"
    #undef ENUM_ENTRY

    ShapeKind_Count,
} ShapeKind;

char *shapeLabels[] = 
{
    #define ENUM_ENTRY(item) "ShapeKind_" STRINGIFY(item) ,
    #include "shape_enum_contents.h"
    #undef ENUM_ENTRY

    "ShapeKind_Count",
};

We’re going to be seeing a lot of the preprocessor from here on out. There will be a deep dive into it a little later if you’re unfamiliar. The crux of this is that storing the items of the enum in one other file that allows the two definitions to exist separately but never diverge.

// shape_enum_contents.h
ENUM_ENTRY(Dot)
ENUM_ENTRY(Line)
ENUM_ENTRY(Circle)
ENUM_ENTRY(Square)

The only cost of this approach is that your project will become littered with files that contain the internal contents of individual enums. And nothing else.

Having tried this approach, I have found that it is ergonomically easiest to deal with enums when their definition effectively lives in one place instead of two. If I want to define the enum ShapeKind in the file geometry.h, I would rather that the definition in geometry.h actually contain the values of the enum without me having to go to another file to check them.

Let’s check back in with our criteria:

  • ✅ Printable
  • ✅ Iterable
  • ✅ Indexable
  • ❌ Closed
  • ❔ Usable as Sentinel

Well that doesn’t look to bad if we ignore the ergonomics.

  • ❌ Ergonomics

Dang.

So, can we do any better while still working in raw c?

Surprisingly, yes. And the technique to do so brings us a lot closer to an enum in c that is usable as a closed, indexable, iterable, printable, usable-as-sentinel set, while being more ergonomic.

An Introduction to Macro-Inversion

Whenever we compose a macro for c, we need to be mindful of what is and isn’t part of the macro. Despite the limitations of the c preprocessor, it is still very powerful, and can be used to do some very helpful things if employed carefully.

In our split implementation from the prior section, the enum is constructed in 3 disjoint and non-colocated parts:

  1. At the site of the definition, boilerplate related to the declaration of the enum type, associating that type with its typename.
typedef enum ShapeKind 
{ 
    /* macro definitions and include 
     * shape_enum_contents.h */ 
} ShapeKind;
  1. Hopefully near the site of the definition, the boilerplate related to defining an array of strings that are used to make the enum printable. If you want to have any other data that is indexable on the enum, it will work similarly to this array.
char *shapeLabels[] = 
{ 
    /* macro definitions and include 
     * shape_enum_contents.h */ 
};
  1. In a separate and dedicated file, the actual list of possible values the enum type can hold.
ENUM_ENTRY(Dot)
// ...

And if we squint our eyes and look carefully, isn’t that laid out exactly the opposite of how we would want it? If we go to the site of ShapeKind’s definition, we will not see the actual values that a ShapeKind can take, but that’s the thing we most likely want to read or alter. What we do see will be the boilerplate of the typedef, enum definition, and array definition, which we are unlikely to ever touch or alter, and which likely does not contain any useful information.

So we’re going to invert this macro, and make the included file contain all the boilerplate, and the definition site contain the values. Our new enum declaration will look a little unusual, but at least it will contain all of the information we actually want, here it is.

#define MACRO_ENUM_NAME ShapeKind
#define MACRO_ENUM_VALUES\
    ENUM_ENTRY(Dot)\
    ENUM_ENTRY(Line)\
    ENUM_ENTRY(Circle)\
    ENUM_ENTRY(Square)

#include "macro-enum.h"

When combined with our implementation in “macro-enum.h”, this implementation will:

  1. Declare a typedef’d enum called ShapeKind, with values ShapeKind_Dot, ShapeKind_Line, ShapeKind_Circle, and ShapeKind_Square.
  2. Define an static inline function char *ShapeKindGetLabel(ShapeKind shape) that returns a printable string for each value of an enum. It will detect and crash when nonsense values are passed in.
  3. Add ShapeKind_Count to the end of the enum to enable easy iteration.

Let’s take a look at “macro-enum.h”, I’ve broken it up for commentary, and this version is abbreviated to the one I actually use.

// JOIN and STRINGIFY macros that will be used in the declaration
#define ENUM_JOIN3(a,b) a ## b
#define ENUM_JOIN2(a,b) ENUM_JOIN3(a,b)
#define ENUM_JOIN(a,b) ENUM_JOIN2(a,b) 
#define ENUM_STRINGIFY2(a) #a
#define ENUM_STRINGIFY(a) ENUM_STRINGIFY2(a)

typedef enum MACRO_ENUM_NAME
{
    // values
    #define ENUM_ENTRY(macro_enum_value) ENUM_JOIN(MACRO_ENUM_NAME, ENUM_JOIN(_, macro_enum_value)),
    MACRO_ENUM_VALUES
    #undef ENUM_ENTRY
    // count
    ENUM_JOIN(MACRO_ENUM_NAME, _Count)
} MACRO_ENUM_NAME;

To get the bottom of this section out of the way, the type definition automatically puts the enum name on the front of every entry, with an underscore, and also adds a _Count item at the end of the enum.

The top begins with macro join and stringify operation definitions that we’ll be using throughout the file, and the type definition. I guess I can’t avoid discussing the preprocessor any longer. You can skip this part if you’re generally aware of how the preprocessor works.

If you’re not familiar with C, the C Preprocessor is used to generate code by way of naive text substitution, with some limits on recursive invocation.

You most likely recognize the preprocessor’s include command from c/c++, used usually with headers to make code from other files accessible. It typically does this by providing shared type definitions.

#include <cstdlib>

This directive takes the entire file cstdlib and pastes its entire contents at the #include. The preprocessor then preprocesses that inserted content.

We’ll be dealing with two of the preprocessor’s operators, and it’s text substitution. Let’s do the text substitution first.

This macro instructs the preprocessor to, from the line after the #define onward, replace any instance it finds of the token ARBITRARY_MACRO_NAME, with the contents that follow it. In this case they would be replaced with int a = 0;.

// the preprocessor sees this, but the compiler doesn't
#define ARBITRARY_MACRO_NAME int a = 0;
// it turns (this thing the compiler never sees)
ARBITRARY_MACRO_NAME
// into (this thing the compiler does see)
int a = 0;

We can also use so called function-like macros to parameterize this process. This function like macro does very similarly, but allows us to use some arguments.

// again, the preprocessor sees this, but the compiler doesn't
#define ARBITRARY_MACRO_FUNC(macro_name, macro_init_val)\
    int macro_name = macro_init_val;
// and it turns (this thing the compiler never sees)
ARBITRARY_MACRO_FUNC(fancy_var, 5)
// into (this thing the compiler does see)
int fancy_var = 5;
// notice how to variable name is determined by the arguments we give the preprocessor.

Alright cool, now let’s talk about the operators. The preprocessor’s # operator, when not at the start of a line, performs stringification. i.e. it turns a token into a string of itself.

#define ARBITRARY_MACRO_NAME #SOME_VALUE OTHER_THING
// turns
ARBITRARY_MACRO_NAME
// into
"SOME_VALUE" OTHER_THING

Without going to deep into the weeds here, the preprocessor is very particular about it’s capture rules, and how tokens are bound to the function-like macro invocations they appear in.

All that is to say, what will the preprocessor do with this:

#define MACRO_ENUM_NAME Thingy
#define MACRO_ENUM_VALUES ENUM_ENTRY(a)
#define ENUM_ENTRY(macro_enum_value) #MACRO_ENUM_NAME "_" #macro_enum_value,
MACRO_ENUM_VALUES

Trick question, the preprocessor tells us that MACRO_ENUM_NAME is not a macro parameter, and halts.

How about now?

#define ENUM_STRINGIFY2(a) #a
#define MACRO_ENUM_NAME Thingy
#define MACRO_ENUM_VALUES ENUM_ENTRY(a)
#define ENUM_ENTRY(macro_enum_value)\
    ENUM_STRINGIFY2(MACRO_ENUM_NAME) "_" #macro_enum_value,
MACRO_ENUM_VALUES

It yeilds

"MACRO_ENUM_NAME" "_" "a", Ah, we need MACRO_ENUM_NAME to be substituted for Thingy, but the operations took place in the wrong order. The stringify happened before the substitution. The solution is just to wrap stringify again. And now we will get the correct answer from this preprocessor code.

#define ENUM_STRINGIFY2(a) #a
// we're adding a level of indirection to STRINGIFY
#define ENUM_STRINGIFY(a) ENUM_STRINGIFY2(a)
#define MACRO_ENUM_NAME Thingy
#define MACRO_ENUM_VALUES ENUM_ENTRY(a1)
#define ENUM_ENTRY(macro_enum_value)\
    ENUM_STRINGIFY(MACRO_ENUM_NAME) "_" ENUM_STRINGIFY(macro_enum_value),
MACRO_ENUM_VALUES

And we finally get the intended

"Thingy" "_" "a1",

In C, consecutive string literals are automatically concatenated, so "Thingy" "_" "a1", is equivalent to "Thingy_a1",

Anyway, similar shenanegans are necessary for the preprocessor’s paste operator ##. The long and the short of it is that the paste operator does this:

#define CONCAT thingA ## _thingB
CONCAT
// yeilds
thingA_thingB

To do that reliably through macro invocations we use the pattern:

#define ENUM_JOIN3(a,b) a ## b
#define ENUM_JOIN2(a,b) ENUM_JOIN3(a,b)
#define ENUM_JOIN(a,b) ENUM_JOIN2(a,b) 

#define THING(thingA, thingB) ENUM_JOIN(thingA,thingB)
THING
// will yield
thingAthingB

That’s your crash course on the preprocessor.

Back to Macro Inversion

We were examining the included file that implements the boilerplate of the enum.

The next section defines functions that convert between the enum’s values and string names.

// the user can #define MACRO_ENUM_NO_GET_LABEL to prevent this 
//  section of the file from being included if they don't want
//  the GetLabel() and GetValueFromLabel() functions
#ifndef MACRO_ENUM_NO_GET_LABEL
// enum string from value
// <Enum>GetLabel()
static inline char *ENUM_JOIN(MACRO_ENUM_NAME, GetLabel)(MACRO_ENUM_NAME value)
{
    char *values[] = {
        #define ENUM_ENTRY(macro_enum_value) ENUM_STRINGIFY(MACRO_ENUM_NAME) "_" ENUM_STRINGIFY(macro_enum_value),
        MACRO_ENUM_VALUES
        #undef ENUM_ENTRY
    };

    // bounds check
    if ( (value < 0) 
        || (value >= ENUM_JOIN(MACRO_ENUM_NAME, _Count)) )
    { 
        // error handling here
        assert(false);
        return 0;
    }

    // return label
    return values[value];
}

GetLabel() uses an array to return a printable label, the members are generated by the user provided MACRO_ENUM_VALUES token.

This version is less error resiliant than the one I use because I didn’t want to complicate this by adding optional types. Normally, the GetLabel() function family would return an optional that allows the user to check if the conversion was successful. The optionals are typically of the forms:

typedef struct MAYBE_int { bool valid; int item; } MAYBE_int;

typedef struct MAYBE_str { bool valid; char *item; } MAYBE_str;

For the respective applicable functions.

The user is ’encouraged’ to check the .valid before accessing .item. For simplicity in the following functions, all errors are assumed to be fatal.

// Enum value from string
// <Enum>GetValueFromLabel()
static inline MACRO_ENUM_NAME ENUM_JOIN(MACRO_ENUM_NAME, GetValueFromLabel)(char *label)
{
    char *values[] = {
        #define ENUM_ENTRY(macro_enum_value) ENUM_STRINGIFY(MACRO_ENUM_NAME) "_" ENUM_STRINGIFY(macro_enum_value),
        MACRO_ENUM_VALUES
        #undef ENUM_ENTRY
    };

    // just run through every value in the enum and check it
    for (int i = 0; i < ENUM_JOIN(MACRO_ENUM_NAME, _Count); i++)
    { 
        if (strcmp(label, value[i]) == 0)
        { return i; }
    }

    // do your error handling here
    assert(false);
    return 0;
}
#endif //MACRO_ENUM_NO_GET_LABEL

This implementation of GetValueFromLabel() is slow, iterating every possible value in the enum to find the one with a matching label. I found this to be of acceptable performance for small and moderate cases.

// undef macros that are created by this file. We can do this 
//  because this file uses its macros within itself and does 
//  not require the user to refer to them. For instance, the 
//  user does not need to write ENUM_JOIN to use the macro in any way.
#undef ENUM_JOIN
#undef ENUM_JOIN2
#undef ENUM_JOIN3
#undef ENUM_STRINGIFY
#undef MACRO_ENUM_VALUES
#undef MACRO_ENUM_NAME

#ifdef MACRO_ENUM_NO_GET_LABEL
    #undef MACRO_ENUM_NO_GET_LABEL
#endif // MACRO_ENUM_NO_GET_LABEL

This last section undefs the macros used only in the file to avoid polluting the global namespace and avoid collisions with definitions of other enums. Since this file is intended for use with every enum, it must not conflict with itself when included multiple times.

Let’s take a look at some usage code.

// definition
#define MACRO_ENUM_NAME ShapeKind
#define MACRO_ENUM_VALUES\
    ENUM_ENTRY(Dot)\
    ENUM_ENTRY(Line)\
    ENUM_ENTRY(Circle)\
    ENUM_ENTRY(Square)

#include "macro-enum.h"

Notice how our enum definition has retained all of the most important and useful information, while still supporting a much more fully featured implementation. Since it doesn’t have to do multiple array and type definitions, it is also more concise.

// usage as sentinel
typedef struct Shape 
{
    ShapeKind shape;
    union
    {
        ShapeDot dot;
        ShapeLine line;
        ShapeCircle circle;
        ShapeSquare square;
    };
} Shape;

// usage as sentinel, v-table/polymorphism like behavior
void DoShapeThings( Shape literal)
{
    switch(literal.shape)
    {
        case ShapeKind_Dot:
        { DoShapeDotThings(literal.dot); } break;
        case ShapeKind_Line:
        { DoShapeLineThings(literal.line); } break;
        case ShapeKind_Circle:
        { DoShapeCircleThings(literal.circle); } break;
        case ShapeKind_Square:
        { DoShapeSquareThings(literal.square); } break;
        default:
        {
            printf("Don't know what to do with shape of kind %s\n", ShapeKindGetLabel(someShape));
        } break;
    }
}

And when we want to use it as a sentinel, clever use of the default case will notify us at runtime if we forgot to include a new case. And fortunately, many compilers will now warn you about detectably missing enum cases in switches if you have verbose warnings enabled. (i.e. -Wall -Wextra on clang.)

// basic usages
int main()
{
    ShapeKind shape = ShapeKind_Line;
    printf("selected shape: %s\n", ShapeKindGetLabel(shape));

    char *shapeName = "ShapeKind_Square";
    ShapeKind shapeKind = ShapeKindGetValueFromLabel(shapeName);

    // Stand-in
    ShapeKind someShape = GetArbitraryShapeKind();

    DoShapeThings(someShape);

    print("Available Shapes:\n");
    for (int i = 0; i < ShapeKind_Count; i++)
    {
        print("\t%s\n", ShapeKindGetLabel(i));
    }
}

Our final collection of examples here show usage of printability and iterability. So how does this implementation measure up to our criteria?

Analysis

Printable ✅

This implementation allows trivial, checked conversion between string and enum type. The implementation does not split core data into multiple places in a way that can get out of sync. I’m calling this one a pass.

Iterable ✅

The implementation provides _Count, which is added automatically to the end of the enum such that changes to the enum will never invalidate it. A for loop using and enum’s _Count as the upper limit is trivial to write.

Normally enums in C are allowed to have non-contiguous members that could not be iterated in this way, but this pattern does not allow for such a thing to be syntactically expressed, because the implementation in “macro-enums.h” assumes contiguous values and does not provide a way to do otherwise.

I’m giving this one a pass as well.

Indexable ✅

As explained under Iterable, this pattern constrains c enums in such a way that prevents non-contiguous values from being expressed syntactically.

For cases where the user wants to map the enum onto arbitrary values, there are better and safer ways to accomplish this. We’ll see that more fully in a future blog post.

Pass.

Closed ❌

The wise programmer can use the facilities of this implementation to write switch statements and other constructs that enforce the closed-ness of the type. However, the programmer must actually write in such a way that enforces this to gain the benefits. If your feature fails when the programmer doesn’t know any better, it’s unprofessional to call that a skill-issue. In team environments, there will be someone who is tired, new, or otherwise doesn’t know any better. Programming style and implementations should be tolerant to the likely human mistakes.

For all the safety measures that this implementation might introduce, the programmer can still write

ShapeKind shapeKind = 0xfafafafa;

This will be caught at runtime if passed to ShapeKindGetLabel(), so that’s something.

But,

when passed into other code paths expecting ShapeKind to be closed set, this will not be caught by the compiler, or at runtime, until something cascades into a critical failure. The programmer is allowed to do this, it is merely a terrible idea.

I did consider wrapping the enum type in a struct that doesn’t freely assign from arbitrary integers, but determined that it likely will not prevent said class of errors and it significantly impedes the ergonomics of everything else.

Naively, this would involve a declaration of the form:

typedef struct ShapeKind { ShapeKindValue v; } ShapeKind;

And initialization in the form:

ShapeKind shape = { .v = ShapeKindValue_Dot };

And in addition to the necessity of having to name both the struct and enum type, the .v = 0 assignment syntax will be common enough to encourage the unwise to make the same mistake in assigning to v.

ShapeKind shape = { .v = /* any arbitrary value */};

If we wanted to avoid this, we could use the macros to automatically define struct instances to make the following syntax valid, but I view that as too much intrusion of macros into regular code. See also the syntax for switch statements or array access.

// this does not spark joy.
#define ShapeKind_Dot ((ShapeKind){ .v = ShapeKindValue_Dot })
ShapeKind shape = ShapeKind_Dot;

Maybe this looks like a good tradeoff to you, but I’m very on the fence about it. I’m not sure I’ve ever made an error of this kind with this pattern, and that makes the effort difficult to justify. Anyway, this one still fails, even though it feels like we got so close. Bummer.

Usable as Sentinel 🤷‍♂️

This one sure seems like a success right? Just write out a switch statement, add a default case that checks for unadded values… We saw this in the dummy function DoShapeThings(). However, humor me, let’s take another look at the sentinel use case:

void DoShapeThings( Shape literal)
{
    switch(literal.shape)
    {// usage as sentinel, v-table/polymorphism like behavior
        case ShapeKind_Dot:
        { DoShapeDotThings(literal.dot); } break;
        case ShapeKind_Line:
        { DoShapeLineThings(literal.line); } break;
        case ShapeKind_Circle:
        { DoShapeLineThings(literal.line); } break;
        case ShapeKind_Square:
        { DoShapeSquareThings(literal.square); } break;
        default:
        {
            printf("Don't know what to do with shape of kind %s\n", ShapeKindGetLabel(someShape));
        } break;
    }
}

That sure is a lot of manually typed boiler-plate. And there’s a lot of room for simple typos to mess things up in there. Did you notice the typo? You certainly can use this pattern to marginally improve your discriminated unions, but it feels to me like there’s some room left on the table here for improvement.

I have experienced an error arising from that kind of typo, it made me want to tear my hair out, and it’s easy to make by copy and paste.

I’m just not sure this provides much value add to the sentinel usage. I still recommend using it over nothing, but it does not speak to the enum pattern’s credit. It doesn’t inhibit it, and does add some typesafety. You could implement this using an array of function pointers if you wished and by bounds-checking it would be similarly safe.

It’s… fine. I’ll give it a 5/10, a thumbs-to-the-side. At the end of the day, this is still c, and discriminated unions aren’t a real first-class feature of the language. Sometimes you just have to take the L.

…

No just kidding,

I didn’t write a blog post about enums to talk about enums. I wrote this blog post as a precursor to my next one.

Are you squinting your eyes again, suspicious that we can do better?

Tune in next time when we dramatically improve this use case, and learn about my submission for most overlooked programming pattern in history.