【问题标题】:What is a nice way to cycle through an enum?循环遍历枚举的好方法是什么?
【发布时间】:2021-07-21 19:30:18
【问题描述】:

背景

对于嵌入式项目中的 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”类来实现这种行为,我随后可以从中派生。这样,我也可以把它写成一个重载的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 类中使用枚举,我又回到了同样的问题:我无法计算 Enum 元素的数量,所以我必须手动指定(我认为这不好,因为它是多余的)。其次,这样我还必须重写派生类中的构造函数,我希望避免它尽可能保持干净。

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

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

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

有没有办法解决这个问题?我在滥用枚举吗?考虑到枚举显然(按设计)如此难以计数的事实,可能。但是,拥有像这样的结构并具有枚举提供的命名值的好方法是什么?

编辑:

好的,我确信在最后使用 _count 元素并不是一个坏主意。不过,我想将其封装在某种结构中,例如类。

我也想过不使用继承,而是使用类模板来完成这个,像这样:

(注意,这不会编译):

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++ 禁止前向引用‘枚举’类型”和“不能在类型说明符中定义匿名枚举”...

是否有另一种方法可以使这个想法发挥作用(使用枚举模板类),或者这不起作用?

【问题讨论】:

  • C++ 的反射能力相当弱。一般来说,如果你想要一个可数的枚举,你会确保它们有连续的值,从 0 开始,并有一个最终的“计数器”枚举值,为此目的清楚地标记了它。类似“count_do_not_use”的东西。有时它会干扰关于switch 语句的编译器警告。这不是一个很好的模式,但它已经广为人知,并且是我们目前拥有的最好的模式。或者,您可以定义所有值的集合。前任。 constexpr Value known_values[] = { /* all the enums*/ }; 但这需要重复所有枚举。
  • 拥有LAST(通常称为count)是个好主意。 “LAST 现在实际上是一个有效的选项” 是的,那又怎样?无论如何,您都可以执行Value(42) 之类的操作。附带说明一下,我会保留将 UPPERCASE_NAMES 用于宏。
  • " 我觉得这很难看,因为 LAST 现在实际上是一个有效的选项。"这实际上是我所知道的最干净的解决方案。考虑基础类型的任何值都是“有效选项”。 MAINCONFIGFOO 等是一些命名常量,但基础类型的任何其他值都是枚举的有效值
  • advanceEnum 通常称为operator++
  • 在你的枚举声明中使用LAST=BAR 让它更好

标签: c++ embedded


【解决方案1】:

问题是,我现在想前进到下一个菜单项。

++ 浮现在脑海。保持简单。

这样,我也可以把它写成一个重载的运算符++

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

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

当然,为什么不呢。这是非常普遍的做法。

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

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

我在滥用枚举吗?

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

【讨论】:

  • ++ 当然是最明显的,但是使用enum class 你不能直接在上面使用++。您需要重载operator++。此外,默认情况下这不会“环绕”。我可以在枚举之外处理这个,但我认为封装这个更干净,所以调用代码不需要担心它。由于我有多个这样的枚举,我认为编写一些实现这两种行为的结构(如类)是有意义的。否则,我将不得不像这样在每个枚举上复制粘贴 operator++()
  • @Compizfox 所以不要使用类。尝试将程序设计得尽可能简单,而不是尽可能复杂。 if(some_enum==MENU_ITEMS_N) some_enum=0; else some_enum++; 是超级简单且非常规范的代码。
  • 旧式enums 是无范围的(因此它们会污染全局命名空间)并且不是类型安全的,这与enum classes 不同。您的解决方案当然很简单,但它没有封装逻辑。它将处理遍历枚举的责任转移到使用枚举的代码上。我不会无缘无故地将我的软件设计得更复杂。根据 OOP 原则,我通常旨在通过编写封装/隐藏此逻辑的类型/类来抽象这样的特定逻辑。不幸的是,对于 C++ 中的枚举,这被证明是棘手的。现在我使用模板函数
【解决方案2】:

您可以使用 magic_enum 库来反映枚举。
将所有枚举元素的名称作为 std::array 并打印出来的示例。

#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

【讨论】:

  • magic_enum 也可以为您提供值列表,即使枚举有(小)间隙。
【解决方案3】:

然后使用_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) ) ;

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

【讨论】:

  • 谢谢,这解决了编译器抱怨switch语句中未处理的枚举值的问题。请注意,使用enum classes,您需要先将它们static_castint,然后才能进行算术运算,因此我的模板函数主体变为:e = static_cast&lt;T&gt;((static_cast&lt;int&gt;(e) + 1) % (static_cast&lt;int&gt;(T::_last) + 1))
  • 是的。我并不打算暗示一个模板。但同样需要演员阵容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-24
  • 2012-11-08
  • 2019-11-20
  • 2011-05-07
  • 1970-01-01
相关资源
最近更新 更多