循环枚举的好方法是什么?

What is a nice way to cycle through an enum?

背景

对于嵌入式项目中的 UI,我正在寻找一种很好的通用方法来存储“状态”并通过按下按钮循环浏览它,例如菜单项列表。

通常,我喜欢为此目的使用枚举,例如:

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

然后,在我的 UI 代码中,我可以存储 currentMenuItem 状态,例如

MenuItem currentMenuItem = MenuItem::MAIN;

并通过将 currentMenuItem 与其枚举中声明的任何可能值进行比较来根据当前状态执行操作。

问题

问题是,我现在想前进到下一个菜单项。为此,我可以非常简单地编写一个函数,通过转换为 int、递增 1,然后将其转换回枚举来实现。我有多个不同的枚举,所以我什至可以使用这个模板函数为任意枚举执行此操作:

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

问题在于它不会回绕:它会愉快地继续增加基础整数,超出实际枚举中的元素数量。我可以很容易地解决这个问题(通过对上述函数中的元素数量取模),只要有一种干净的方法来获取枚举的元素计数。据我所知,实际上并没有。

自定义枚举?

我正在考虑编写一个自定义 'CyclicEnum' class 来实现此行为,我随后可以从中派生。这样,我也可以将其写为重载 operator++.

但是,我仍然没有想出如何在不实际使用枚举的情况下获得类似枚举的东西。例如,我得到了这样的东西:

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) {}
};

但是,如您所见,这仍然在自定义 CyclicEnum class 中使用枚举,我又回到了同样的问题:我无法计算枚举元素的数量,所以我必须手动指定(我认为这不好,因为它是多余的)。其次,通过这种方式,我还必须重写派生 class 中的构造函数,我想避免它尽可能保持干净。

搜索这个问题后,许多人显然建议在枚举末尾添加一个“虚拟”值作为获取大小的技巧:

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

但坦率地说,我觉得这很难看,因为 _count 现在实际上是一个有效的选项。

有什么办法解决这个问题吗?我在滥用枚举吗?看看枚举显然(按设计)很难计算的事实,可能。但是,如果使用枚举提供的命名值来拥有这样的结构,最好的方法是什么?

编辑:

好的,我相信在末尾使用 _count 元素并不是一个坏主意。尽管如此,我还是想将其封装在某种结构中,例如 class.

我还想过不使用继承,而是使用 class 模板来完成此操作,如下所示:

(注意,这不会编译):

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,
}> {};

但是,这不起作用,因为“ISO C++ 禁止转发对 'enum' 类型的引用”和“不能在类型说明符中定义匿名枚举”...

是否有另一种方法可以使这个想法可行(使用枚举模板 class),或者这行不通?

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

想到

++。保持简单。

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

是的......或者再一次,保持简单,你可以放弃整个 class。没有必要围绕一个简单的整数编写抽象层。真的。

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

当然可以。这是非常普遍的做法。

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

这是规范的代码,并不难看。只需给它起一个合理的名称,比如 MENU_ITEMS_N 来暗示这是一个计数器变量,然后就这样使用它。 for(int i=0; i<MENU_ITEMS_N; i++) ...

Am I abusing enums?

枚举只是命名的整数值。不要过度设计您的代码。这对性能不利,对维护不利,它增加了不必要的复杂性。

您可以使用 magic_enum 库来反映枚举。
将所有枚举元素的名称获取为 std::array < std::string_view > 并打印它们的示例。

#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
}

打印:
蜜脆
富士
羡慕

有一些限制,请阅读magic enum limitations

然后使用 _count,将最后一个“标记”值设置为最后一个实际值。

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

这样就避免了 enum 值不是有效菜单选项的问题。例如,以您的增量代替 :

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

你会:

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

如果实际上这些枚举只是导致调用不同的菜单项处理函数,那么您可以改为使用指向函数的数组,而不是 enum/switch 或其他任何东西。