【问题标题】:Requirements of the inequality operator of an input/output iterator输入/输出迭代器不等式运算符的要求
【发布时间】:2023-03-30 14:15:01
【问题描述】:

我正在创建一个基于图块的小游戏。游戏中的项目将它们的位置存储在一个桶矩阵中。我已经将它实现为一个名为 Grid 的类模板,其中包含一个名为 Tile 的存储桶类。

Grid 本质上只是std::vector 的一个包装器,带有用于将坐标转换为索引键的各种访问器方法。它还转发向量的迭代器,以便我可以遍历 Grid 中的所有 Tiles

虽然有时我只需要遍历Grid 的一个小节。所以我实现了一个名为Section 的小类,它在构造函数中使用两组坐标来定义一个AABB。 Sectionbegin()end() 方法返回输入/输出迭代器,用于循环遍历 AABB 内的所有图块。

一切正常,但我试图使迭代器的性能尽可能接近嵌套循环。基本上使用基于Section 的范围不应该比:

for (size_t y = 0, end_y = NUM; y < end_y; ++y)
{
    for (size_t x = 0, end_x = NUM; x < end_x; ++x)
    {
        auto& tile = grid[coords_to_key(x, y)];
    }
}

这让我想到了问题的重点。我希望不等式运算符尽可能简单,所以我这样实现它:

bool operator!=(const Section_Iterator& other) const
{
    return m_coords.y < other.m_coords.y;
}

由于迭代器按顺序扫描Section 中的每一行,我们知道当iterator.y &gt;= end.y 时我们已经“结束”了。这意味着我的不等式运算符适用于基于范围的 for 循环,因为在后台他们只是检查 iterator != end

虽然操作符的实现看起来很奇怪。就像真的很奇怪。例如iterator != ++iterator 可能是真或假。这取决于预增量是否导致迭代器跳转到下一行。

我一直在研究标准,我认为我很清楚,因为它们区分了平等和等价。

来自http://en.cppreference.com/w/cpp/concept/InputIterator

注意,“在 == 的域中意味着相等比较是在两个迭代器值之间定义的。对于输入迭代器,不需要对所有值都定义相等比较,== 域中的值集合可能会随时间变化。

来自http://en.cppreference.com/w/cpp/concept/OutputIterator

不能为输出迭代器定义等式和不等式。即使定义了 operator==,x == y 也不必暗示 ++x == ++y。

不过,老实说,标准语让我头晕目眩。我做的事合法吗?

【问题讨论】:

  • 我认为您过早地进行了优化,应该使用最简单的实现来保留 != 运算符的预期语义并且不会让您头疼
  • 这是最简单的实现。它只是不一定是 正确的 实现。我试图找出标准中关于输入/输出迭代器的!= 运算符的正确语义是什么,因为它不太清楚。
  • 我的意思是,为什么不只是 return m_coords.y != other.m_coords.y; ?如果您甚至没有证明它是性能瓶颈,为什么还要让它复杂化呢?
  • 因为它不适用于浮点坐标。
  • 我不确定我是否理解。该部分包含最小和最大浮点坐标。迭代器递增一组浮点坐标,然后在取消引用迭代器时将其转换为向量的索引。我不能只使用向量迭代器,因为瓦片都存储在同一个向量中,一行接一行。部分迭代器必须评估它是否已到达部分行的末尾,跳过网格行中的其余图块,然后找到下一个部分行的开始。如果有帮助,我可以将预增量运算符编辑到问题中。

标签: c++ iterator equality equivalence


【解决方案1】:

经过更多研究发现,根据标准,我所做的事情是不合法的。

输入迭代器必须是EqualityComparable。这意味着:

  • 对于 a 的所有值,a == a 的结果为 true。
  • 如果 a == b,则 b == a
  • 如果 a == b 且 b == c,则 a == c

对于我当前的相等运算符a == b 并不意味着b == a

为了解决我查看std::istream_iterator 的问题,它是一个输入迭代器的实现,自然它所做的任何事情都必须符合标准。其相等运算符的行为描述如下:

检查 lhs 和 rhs 是否相等。如果两个流迭代器都是流尾迭代器,或者它们都引用同一个流,则两个流迭代器相等

基本上,如果两个迭代器都有效,它们比较相等。如果它们都“结束”,则它们比较相等。如果一个是有效的,但一个是“结束”,则它们不相等。

对我的Section::iterator 应用相同的逻辑很容易。迭代器现在包含一个布尔值m_valid。方法begin() 总是返回一个迭代器,其中m_valid == trueend() 方法总是返回一个迭代器,其中m_valid == false

迭代器的预增量运算符现在测试它是否超过末尾并相应地设置布尔值。

Section_Iterator& operator++()
{
    ++m_coords.x;
    if (m_coords.x >= m_section.m_max.x)
    {
        m_coords.x = m_section.m_min.x;
        ++m_coords.y;
        m_valid = (m_coords.y < m_section.m_max.y);
    }

    return *this;
}

等式运算符现在非常易于理解并且具有一致的行为。任何指向Section 中的Tile 的迭代器都是有效的,并且与任何其他有效的迭代器比较相等。

bool operator==(const Section_Iterator& other) const
{
    return m_valid == other.m_valid;
}

bool operator!=(const Section_Iterator& other) const
{
    return m_valid != other.m_valid;
}

【讨论】:

    【解决方案2】:

    老实说,我不知道您在上面所做的是否合法。不过,它当然有奇怪的语义,即使它是合法的。

    相反,我会考虑这样的事情来解决您的问题:

    #include <iostream>
    #include <vector>
    
    struct Grid
    {
        std::vector<int> tiles;
        size_t rows;
        size_t cols;
    };
    
    class SectionIterator
    {
    public:
        SectionIterator(Grid * grid, size_t row, size_t col, size_t endRow) :
                m_row{ row },
                m_col{ col },
                m_startRow{ row },
                m_endRow{ endRow },
                m_grid{ grid }
        {
        }
    
        SectionIterator & operator++()
        {
            ++m_row;
            if (m_row == m_endRow)
            {
                m_row = m_startRow;
                ++m_col;
            }
            return *this;
        }
    
        bool operator==(const SectionIterator & other) 
        {
            return (m_grid == other.m_grid) 
                    && (m_row == other.m_row)
                    && (m_col == other.m_col);
        }
    
        bool operator!=(const SectionIterator & other) 
        {
            return !(*this == other);
        }
    
        int & operator*() 
        {
            return m_grid->tiles[m_col * m_grid->rows + m_row];
        }
    
        int * operator->() 
        {
            return &operator*();
        }
    private:
        size_t m_row;
        size_t m_col;
        size_t m_startRow;
        size_t m_endRow;
        Grid * m_grid;
    
    };
    
    struct Section 
    {
        SectionIterator m_begin;
        SectionIterator m_end;
    
        SectionIterator begin() { return m_begin; }
        SectionIterator end() { return m_end; }
    };
    
    int main() 
    {
        Grid grid{ std::vector<int>{ 1, 2, 3, 4, 5, 6 }, 2, 3 };
        // 1, 3, 5 
        // 2, 4, 6
    
        // look up start and end row and col
        // end positions are found by looking up row/col of section end and then adding one
        size_t startRow = 0;
        size_t endRow = 2;
        size_t startCol = 1;
        size_t endCol = 3;
    
        SectionIterator begin = SectionIterator{ &grid, startRow, startCol, endRow };
        // Note that the end iterator actually has startRow as its startRow, not endRow, because operator++ will set the iterator's m_row back to startRow so this will make it equal the end iterator once the iteration is complete
        SectionIterator end = SectionIterator{ &grid, startRow, endCol, endRow };
        for (int v : Section{ begin, end })
        {
            std::cout << v << std::endl;
        }
        return 0;
    }
    

    请注意,这假定您有一些函数可以在网格中的坐标和行/列索引之间进行转换。此外,上述以列优先顺序进行迭代,但可以轻松更改为以行优先顺序进行迭代。

    编辑

    为了阐明从浮点坐标到索引的转换是如何工作的,请考虑以下内容。

    我假设您的图块被定义为每个图块覆盖一个 1x1 平方的浮点坐标。例如,tile (0, 0) 覆盖浮点区间 [0.0, 1.0), [0.0, 1.0),tile (2, 2) 覆盖区间 [2.0, 3.0), [2.0, 3.0)。我相信这就是您描述当前设置的方式。

    如果您希望从 (1.2, 1.2) 到 (4.2, 4.2) 迭代该部分中的所有图块,请首先通过截断将这些点转换为行、列索引:

    (1.2, 1.2) = 平铺 (1, 1) (4.2, 4.2) = 平铺 (4, 4)

    这意味着您要迭代闭闭区间 [1, 4] 中的行和闭闭区间 [1, 4] 中的列。由于像上面这样的迭代器使用闭开区间,您必须将 1 添加到结束索引,这样您传递给迭代器的值代表行的区间 [1, 5) 和列的区间 [1, 5)。请注意,这些区间实际上与闭闭区间形式相同,但结束值表示“超过您要取消引用的最后一个索引”。

    编辑#2

    您表示您实际上希望确保您的部分在浮点坐标中的开放间隔上完成,因此 (1.0, 1.0) 到 (4.0, 4.0) 包含 3 个平铺行和 3 个平铺列,而不是 4 个。

    您可以通过将结束索引与原始值进行比较来做到这一点,如果它不是整数则只加 1,所以

    float coord = ...;
    size_t idx = static_cast<size_t>(coord);
    constexpr float tolerance = 1e-6f;
    if (std::abs(coord - idx) > tolerance)
    {
        // Add 1 to make the range include the last tile
        ++idx;
    }
    

    【讨论】:

    • 不像问题文本本身所说的那样,但在弄清楚你想要做什么之后,我相信这可以解决你的问题。
    • 这只是我已经拥有的更有限的实现。使用它会产生更多问题。我必须 floorceil 我的浮点坐标,然后再定义一个会产生大量开销的部分,只是为了执行 for loop
    • 但是您说您当前正在取消引用运算符中进行完全相同的转换,这意味着您必须在循环的每次迭代中进行。此解决方案仅在开始迭代之前执行一次。确定这样更有效率吗?
    • 不,在解引用运算符中,我通过转换为 size_t 来截断。这比ceil 快很多。
    • 也许您对某些事情感到困惑:在上面,我是说使用size_t 作为迭代计数器而不是浮点值。这样,您可以在迭代开始之前执行一次转换。而且您不必处理浮点比较和奇怪的operator!= 语义。我已经从我的代码中省略了浮点值,因为一旦执行了转换,它们就与它的操作无关。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-09
    • 1970-01-01
    相关资源
    最近更新 更多