5

Background

For a UI in an embedded project, I'm looking for a nice generic way to store a "state" and cycle through it with a button press, e.g. a list of menu items.

Normally, I like to use enums for this purpose, for example:

enum class MenuItem {
    main,
    config,
    foo,
    bar,
};

Then, in my UI code, I can store a currentMenuItem state like

MenuItem currentMenuItem = MenuItem::MAIN;

and do things depending on the current state by comparing currentMenuItem to any of its possible values, as declared in the enum.

Problem

The thing is, I would now like to advance to the next menu item. For that, I can quite trivially write a function that does that by casting to int, incrementing by one, and casting it back to the enum. I have multiple distinct enums, so I can even use this templated function that does this for arbitrary enums:

template <typename T>
void advanceEnum(T &e) {
    e = static_cast<T>(static_cast<int>(e) + 1);
}

The problem with this is that it doesn't wrap around: it will happily keep increasing the underlying integer beyond the number of elements in the actual enum. I could quite easily solve this (by taking the modulo with the number of elements in the above function), if only there was an clean way to get the element count of an enum. Which, as far as I could find, there isn't really.

Custom enum?

I was thinking of writing a custom 'CyclicEnum' class that implements this behaviour, that I can subsequently derive from. That way, I could also write this as an overloaded operator++.

However, I still haven't devised how to can get an enum-like thing without actually using an enum. For example, I got to something like this:

class CyclicEnum {
public:
    uint8_t v;

    CyclicEnum& operator++() {
        v = (v+1) % count;
        return *this;
    }

    CyclicEnum operator++(int) {
        CyclicEnum old = *this;
        operator++();
        return old;
    }
private:
    uint8_t count;

protected:
    CyclicEnum(uint8_t v, uint8_t count) : v(v), count(count) {}
};

struct Tab : public CyclicEnum {
    enum Value {
        main,
        config,
        foo,
        bar,
    };

    Tab(Value v) : CyclicEnum(v, 4) {}
};

However, as you can see, this still uses an enum inside the custom CyclicEnum class, and I'm back to the same issue: I can't count the number of Enum elements, so I have to specify that manually (which I think is not nice because it's redundant). Secondly, this way I also have to override the constructor in the derived class which I would like to avoid to keep it as clean as possible.

Upon searching this issue, many people apparently recommend to add a "dummy" value at the end of the enum as a trick to get the size:

enum Value {
    main,
    config,
    foo,
    bar,
    _count,
};

but frankly, I find that just ugly, since _count is now actually a valid option.

Is there any way around this? Am I abusing enums? Looking at the fact that enums are apparently (by design) so hard to count, probably. But what would be a nice way to have a structure like this with named values like an enum provides?

EDIT:

OK, I'm convinced that using a _count element at the end isn't so bad of an idea. Still, I'd like to encapsulate this in some kind of structure, like a class.

I also thought of instead of using inheritance, using a class template to accomplish this, something like this:

(caution, this doesn't compile):

template<typename T>
struct CyclicEnum {
    T v;
    enum Values = T;

    CyclicEnum& operator++() {
        v = (v+1) % T::_count;
        return *this;
    }

    CyclicEnum operator++(int) {
        CyclicEnum old = *this;
        operator++();
        return old;
    }
    
    CyclicEnum(T v) : v(v) {}
};

struct MenuItem : public CyclicEnum<enum class {
    main,
    config,
    foo,
    bar,
    _count,
}> {};

However, this does not work because "ISO C++ forbids forward references to 'enum' types" and "anonymous enum cannot be defined in a type specifier"...

Is there another way to make this idea work (template a class with an enum), or is this not going to work?

datafiddler
  • 1,755
  • 3
  • 17
  • 30
Compizfox
  • 748
  • 2
  • 7
  • 17
  • 2
    C++ has rather weak reflection capabilities. Generally, if you want a countable enum, you would make sure they have consecutive values, start at 0 and have a final "counter" enum value that is clearly labeled for this purpose. Something like "count_do_not_use". Sometimes it interferes with compiler warnings regarding `switch` statements. This isn't a great pattern, but it is pretty widely known and the best we have at the moment. Alternatively, you can define a collection of all values. ex. `constexpr Value known_values[] = { /* all the enums*/ };` but this requires repeating all the enums. – François Andrieux Jul 21 '21 at 19:34
  • Having `LAST` (more commonly named `count`) is a good idea. *"LAST is now actually a valid option"* Yeah, so what? You can do stuff like `Value(42)` regardless. On a side note, I would reserve the use of `UPPERCASE_NAMES` for macros. – HolyBlackCat Jul 21 '21 at 19:34
  • " I find that just ugly, since LAST is now actually a valid option." this is actually the cleanest solution I am aware of. Consider that any value of the underlying type is "a valid option". `MAIN`, `CONFIG`, `FOO` etc, are some named constants, but any other value of the underlying type is a valid value of the enum – 463035818_is_not_an_ai Jul 21 '21 at 19:36
  • `advanceEnum` is more commonly called `operator++`. – Pete Becker Jul 21 '21 at 19:39
  • 1
    use `LAST=BAR` in your enum declaration to make it nicer – C.M. Jul 21 '21 at 19:52
  • @FrançoisAndrieux Thanks, yes. I agree that it isn't a great pattern, also because I can't 'hide' it from the derived classes. – Compizfox Jul 21 '21 at 21:20
  • @HolyBlackCat,463035818_is_not_a_number Thanks, that is fair. I hadn't considered that actually any value of the underlying type is a valid option, as far as the compiler is concerned. – Compizfox Jul 21 '21 at 21:22
  • There may be some good answers for you here: [How can I iterate over an enum?](https://stackoverflow.com/questions/261963/how-can-i-iterate-over-an-enum) – Brandin Jul 26 '21 at 10:29

3 Answers3

4

The thing is, I would now like to advance to the next menu item.

++ comes to mind. Keep it simple.

That way, I could also write this as an overloaded operator++

Yeah... or again, keep it simple, you could just drop the whole class. There's no need to write an abstraction layer around a simple integer. Really.

Upon searching this issue, many people apparently recommend to add a "dummy" value at the end of the enum as a trick to get the size

Sure why not. This is incredibly common practice.

but frankly, I find that just ugly, since LAST is now actually a valid option.

It's canonical code, it isn't ugly. Just give it a sensible name, maybe something like MENU_ITEMS_N to suggest that this a counter variable then use it as such. for(int i=0; i<MENU_ITEMS_N; i++) ...

Am I abusing enums?

Enums are just named integer values. Don't over-engineer your code. It's bad for performance, it's bad for maintenance, it adds needless complexity.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • `++` would of course be most obvious, but with an `enum class` you cannot directly use `++` on it. You would need to overload `operator++`. Additionally, this does not 'wrap around' by default. I could handle this outside of the enum, but I think it is cleaner to encapsulate this, so the calling code doesn't need to worry about it. Since I have multiple enums like this, I think it makes sense to write some structure (like a class) that implements both of these behaviours. Otherwise, I would have to copy-paste the `operator++()` on every enum like this. – Compizfox Jul 21 '21 at 21:27
  • @Compizfox So don't use a class. Try to design programs as _simple_ as possible, not as complicated as possible. `if(some_enum==MENU_ITEMS_N) some_enum=0; else some_enum++;` is super simple and pretty canonical code. – Lundin Jul 22 '21 at 20:29
  • Old-style `enum`s are unscoped (so they pollute the global namespace) and not type safe, unlike `enum class`es. Your solution is certainly simple, but it doesn't encapsulate the logic. It shifts the responsibility of dealing with cycling through the enum to the code where the enum is used. I'm not trying to design my software more complicated for no reason. In line with OOP principles, I typically aim to abstract specific logic like this by writing a type/class that encapsulates/hides this logic. Unfortunately, with enums in C++, this proved tricky. For now I use the templated function – Compizfox Jul 26 '21 at 16:19
3

You could use the magic_enum library to reflect on enums.
Example which gets the names of all enum elements as std::array < std::string_view > and prints them.

#include <algorithm>
#include <iostream>
#include <magic_enum.hpp>

enum struct Apple
{
  Fuji = 2,
  Honeycrisp = -3,
  Envy = 4
};

int
main ()
{
  constexpr auto &appleNames = magic_enum::enum_names<Apple> ();                                                 // get an std::array<std::string_view> with the names for the enum sorted by value
  std::copy (appleNames.begin (), appleNames.end (), std::ostream_iterator<std::string_view> (std::cout, "\n")); // print all the names
}

Prints:
Honeycrisp
Fuji
Envy

There are some limitations please read magic enum limitations

Koronis Neilos
  • 700
  • 2
  • 6
  • 20
1

Rather then using _count, set the last "sentinel" value to the last actual value.

enum Value {
    main,
    config,
    foo,
    bar,
    last = bar
};

Then you avoid the problem of having an enum value that is not a valid menu option. in your increment for example instead of :

v = static_cast<Value>( (static_cast<int>(v) + 1) % 
                        static_cast<int>(Value::_count) );

you'd have:

v = static_cast<Value>( (static_cast<int>(v) + 1) %
                        (static_cast<int>(Value::last) + 1) ) ;

If in fact these enums simply cause different menu item handler functions to be called, then you could instead used an array of pointer-to-functions rather then an enum/switch or whatever.

Clifford
  • 88,407
  • 13
  • 85
  • 165
  • Thank you, this solves the issue of the compiler complaining about unhandled enum values in switch statements. Note that with `enum class`es you need to `static_cast` them to `int` before you can do arithmetic, so the body of my templated function becomes: `e = static_cast((static_cast(e) + 1) % (static_cast(T::_last) + 1))`. – Compizfox Jul 26 '21 at 16:06
  • Yes. I did not intend to imply a template. But equally the cast is required. – Clifford Jul 27 '21 at 08:41