Better Enums

Reflective compile-time enums for C++

Open-source under the BSD license

Version 0.11.3

To install, just add enum.h to your project.

Visit the GitHub repo for issues, feedback, and the latest development.

Download enum.h GitHub

This is an example of code you can write on top of Better Enums. It's a valid program — you can download it and try it out. The program is also part of the test suite.

Semi-quine

Let's make a Better Enum assemble its own definition in memory. It won't be literally as defined, since we will lose the exact initializer expressions, but we will be able to preserve the numeric values. We will reserve the memory buffer for the definition at compile time.

Ok, so it's not really a quine, because we won't be writing all the code needed to generate the definition to the buffer as well. And, there are better ways to dump the definition than shown here. You could simply define a macro that expands to an BETTER_ENUM declaration and also stringizes it.

But that's not the point here. The point of this page is to show some of the reflective capabilities of Better Enums, so you can adapt them for cases where a macro is not sufficient :)

Contents


#include <cassert>
#include <cstdio>
#include <iostream>

First, we will need full compile-time reflection, since we will be calling _to_string. Let's make sure it's enabled by defining BETTER_ENUMS_CONSTEXPR_TO_STRING before including enum.h:

#ifndef BETTER_ENUMS_CONSTEXPR_TO_STRING
#define BETTER_ENUMS_CONSTEXPR_TO_STRING
#endif

#include <enum.h>

Now, let's declare some enums to dump later:

BETTER_ENUM(Channel, int, Red, Green, Blue)
BETTER_ENUM(Depth, int, TrueColor = 1, HighColor = 0)

Computing the size of the buffer

First, we need to be able to get the length of each declaration above. We will assume that the underlying type is always int, and that the spacing convention is followed as above.

First, let's get the lengths of basic components:

// Returns the length of the string representation of the number n
constexpr size_t value_length(int n, int bound = 10, size_t digits = 1)
{
    return
        n < bound ? digits : value_length(n, bound * 10, digits + 1);
}

// Returns the length of s
constexpr size_t string_length(const char *s, size_t index = 0)
{
    return s[index] == '\0' ? index : string_length(s, index + 1);
}

Now, the length of the constant declaration. Here is where we lose information about initializers. We are going to format the constant declarations like this:

Red = 0, Green = 1, Blue = 2
TrueColor = 1, HighColor = 0

This is because Better Enums doesn't provide a way to know what the exact initializer was or whether there even was one — just the numeric value of each constant. If we were trying to be clever, we could avoid formatting initializers for sequential values, but I won't go through this exercise here.

// Returns the length of the constants portion of the declaration of Enum,
// as described above.
template <typename Enum>
constexpr size_t constants_length(size_t index = 0, size_t accumulator = 0)
{
    return
        index >= Enum::_size() ? accumulator :
        constants_length<Enum>(
            index + 1, accumulator
                        + string_length(", ")
                        + string_length(Enum::_names()[index])
                        + string_length(" = ")
                        + value_length(
                            Enum::_values()[index]._to_integral()));
}

Finally, we can combine these to get the length of the formatted declaration of the whole enum:

// Returns the length of the whole declaration of Enum, assuming the
// underlying type is int, and the constants are initialized as assumed by
// constants_length() above.
template <typename Enum>
constexpr size_t declaration_length()
{
    return
        string_length("BETTER_ENUM(")
        + string_length(Enum::_name())
        + string_length(", int")
        + constants_length<Enum>()
        + string_length(")");
}

Formatting the enums

Now, we can declare the buffers. The memory will be reserved at load time by the binary's loader. The extra one byte in each buffer is for the null terminator.

char    channel_definition[declaration_length<Channel>() + 1];
char    depth_definition[declaration_length<Depth>() + 1];

Let's also create the formatting function. This is executed at run time, but we will be giving it pointers to our statically-allocated buffers. It will format the enum declaration and then return the number of bytes it wrote to the buffer, so that we can do a sanity check on it.

template <typename Enum>
size_t format(char *buffer)
{
    size_t  offset = 0;

    offset += std::sprintf(buffer, "BETTER_ENUM(%s, int", Enum::_name());

    for (Enum value : Enum::_values()) {
        offset +=
            std::sprintf(buffer + offset,
                         ", %s = %i",
                         value._to_string(), value._to_integral());
    }

    offset += std::sprintf(buffer + offset, ")");

    return offset;
}

Checking our work

Now, we can write and run this code.

int main()
{
    size_t  channel_length = format<Channel>(channel_definition);
    assert(channel_length + 1 == sizeof(channel_definition));

    size_t  depth_length = format<Depth>(depth_definition);
    assert(depth_length + 1 == sizeof(depth_definition));

    std::cout << channel_definition << std::endl;
    std::cout << depth_definition << std::endl;

    return 0;
}

It prints:

BETTER_ENUM(Channel, int, Red = 0, Green = 1, Blue = 2)
BETTER_ENUM(Depth, int, TrueColor = 1, HighColor = 0)