枚举方向并进行迭代

5

我有一个包含6个方向条目(N,NE,SE,S,SW,NW)的方向枚举,基本上是图中节点的边缘。我经常需要迭代所有邻居,目前通过仅使用int从0-5进行迭代来完成。

有时需要从例如2->1进行迭代,可以遍历回来,目前方法是从2->2+5进行迭代,并在使用时对其进行mod 6运算。

是否有任何方法可以使其更安全、易于使用,同时不会失去性能?具有固定整数范围的for循环可以展开、内联等。基于向量的方法则不行(在枚举内使用静态const向量)

我已经按照以下方式对枚举进行了设计:

struct Direction{
    enum Type{
        N, NE, ...
    }
    unsigned COUNT = ...;
    Type t_;
    operator Type(){return t_;}
    Direction(Type t):t_(t){assert(N<=t<COUNT);}
    explicit Direction(unsigned t):t_(t%COUNT){}
    static Direction fromUInt(unsigned t){return Direction(Type(t));}
}

所以我想要的是拥有迭代器,可以高效地遍历整个集合,并且也允许从任意起始点开始遍历,如果是这种情况,迭代器会绕回。

如何编写这个呢?我无法想出任何办法,因为在整个循环中,例如start=end,这将是无效的。或者我应该使用start=givenStartType,end=start+COUNT并在每个迭代器 deref 上执行模数操作吗?

不幸的是,不允许使用C ++ 11


没有可用的代码在问题中。发布的代码仅仅是一个枚举结构,问题所涉及的内容。在代码下面发布的是一些起始想法,而不是更多的东西。 - Flamefire
2个回答

2

针对澄清的修改

您关于在每次解除引用时将迭代器取模COUNT的想法是一个好主意。请参见下面的反向迭代器/可迭代组合。我使用-O3编译器编译后检查了汇编输出。编译器展开了循环。输出为2 1 0 5 4 3。您可以实现一个正向迭代器,或将方向作为参数。您还可以将其制作成枚举类型的模板。

不幸的是,在使用语法方面,我认为这与另一个答案中显示的do-while循环相比,并没有带来太多好处-至少在C++11之前是如此。它确实隐藏了各种索引变量,并帮助您避免出错,但更加冗长。

#include <iostream>

struct Direction {
    enum Type {N, NE, SE, S, SW, NW};

    static const unsigned COUNT = 6;

    Type t_;

    operator Type() { return t_; }
    Direction(Type t) : t_(t) { }
    explicit Direction(unsigned t) : t_(Type(t % COUNT)) { }
};

struct ReverseIterable {
    const unsigned      start_;

    struct iterator {
        unsigned        offset_;

        explicit iterator(unsigned offset) : offset_(offset) { }

        Direction operator *() { return Direction(offset_); }
        iterator& operator++() { --offset_; return *this; }

        bool operator ==(const iterator &other)
            { return offset_ == other.offset_; }
        bool operator !=(const iterator &other) { return !(*this == other); }
    };

    explicit ReverseIterable(Direction start) : start_(start) { }

    iterator begin() { return iterator(start_ + Direction::COUNT); }
    iterator end() { return iterator(start_); }
};

int main()
{
    ReverseIterable     range = ReverseIterable(Direction::SE);

    for (ReverseIterable::iterator iterator = range.begin();
         iterator != range.end(); ++iterator) {

        std::cout << (int)*iterator << " ";
    }
    std::cout << std::endl;

    return 0;
}

在C++11中,循环可以是这样的:
    for (Direction direction : ReverseIterable(Direction::SE))
        std::cout << (int)direction << " ";
    std::cout << std::endl;

“您可以使用宏在C++98中实现类似的功能。” “我暂时保留了原始答案,因为如果枚举定义发生更改,则可以更轻松地进行维护,而且它允许稀疏范围。非常相似的迭代器可以在其基础上实现。”

原始答案专注于安全

这可能对您的目的来说有些过度,也可能由于我将在下面进一步描述的原因而不合适。然而,您可以使用这个库(免责声明:我是作者):https://github.com/aantron/better-enums编写如下代码:

#include <iostream>
#include <enum.h>

ENUM(Direction, int, N, NE, SE, S, SW, NW)

int main()
{
    size_t  iterations = Direction::_size();
    size_t  index = 2;

    for (size_t count = 0; count < iterations; ++count) {
        std::cout << Direction::_values()[index] << " ";
        index = (index + 1) % Direction::_size();
    }
    std::cout << std::endl;

    return 0;
}

输出:

SE S SW NW N NE

这段代码中,枚举值是 int 类型的,但只有在输出到 std::cout 时才转换为字符串。
这展示了对整个集合的迭代,起始点是任意的。您可以使其向前或向后移动,并将其模板化为任何枚举类型。
我认为使用取模运算符就像您的问题一样是一个好主意。这段代码仅提供有关枚举常量数量的反射信息,并将它们放入数组中。
可能不适合的原因是,由于您没有使用 C++11,数组 Direction::_values() 将在程序启动时初始化。我认为循环展开仍然可以发生,但编译器无法处理数组的内容。数组仍将被静态分配,但元素在编译期间不可用。
如果您以后有使用 C++11 的选项,则该数组基本上与静态初始化的 int[6](实际上是 Direction[6],其中 Direction 是一个字面量的 struct 类型)相同。
实际上,我想我可以暴露一个int数组而不是Direction,这将在C++98中静态初始化。

我已经有两个构造函数,接受整数参数并将其转换为Direction。所以我在这里看不到任何好处。特别是因为我还需要几个变量(至少count和index)。请参考修改后的问题。 - Flamefire
我明白了。我认为我(和另一个回答者)一直在关注您问题中的安全性部分,即我们假设的枚举定义的更改抵抗力。我将编辑问题以勾勒出迭代器。 - antron
实际上,看起来你的问题对于迭代器已经有了正确的想法——在每次解引用时执行模运算。正如我所提到的,我认为这是正确的做法。重点是将上面的代码移入函数或将其分割成迭代器。我仍然会给出一个例子,但“在每次解引用时执行模运算”作为答案是否足够呢?由于你已经有了COUNT并且似乎假设密集的范围,那么安全性的剩余问题是什么? - antron
看起来很不错。非反向可迭代应该足够了,但反向是一个奖励。我会禁止从int构造Direction以避免扩展模数,并在迭代器中执行。避免负数的一个想法是使用start+COUNT作为起始点。你可能可以做类似这样的事情:for (ReverseIterable::iterator iterator = Direction::Iterable(Direction::NE).begin(); iterator != Direction::Iterable(Direction::NE).end(); ++iterator) 或者使用宏来模拟C++11循环。 - Flamefire
好的建议。我根据它们改变了答案中的代码,除了宏 - 我只是提到了它,因为推荐它可能会引起争议 :) 另外,如果你认为在答案中使用反射库是一个不好的想法,那么如果你编辑你的问题以表明常量范围被假定为密集/连续,我会删掉那段文字,以保持答案简洁。这仅适用于其他可能在此时有类似问题的用户。 - antron

0

如果你想避免使用自定义库,通常采用的方法是这样的:

enum Direction
{
    SE,
    S,
    SW,
    NW,
    N,
    NE,

    DIRECTION_FIRST = SE,
    DIRECTION_LAST = NE,
}

然后,您可以使用DIRECTION_FIRSTDIRECTION_LAST来正确遍历范围。如果某人在不更新迭代结束点的情况下向枚举添加值,则仍有出错的余地,但是将其集中在枚举中应该会减少这种情况的发生。

现在,如果您假设任意起始方向start,则可以像这样进行迭代:

Direction current = start;
do
{
    // Do stuff with current...

    current = (current + 1) % DIRECTION_LAST;
} while(current != start);

如果你的枚举不是从0开始,那么逻辑会变得更复杂一些,但仍然是可能的。这大概是唯一需要使用DIRECTION_FIRST的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接