【问题标题】:Decrementable requirements of general end iterators vs container `end()`一般结束迭代器与容器`end()`的可递减要求
【发布时间】:2021-04-10 21:07:20
【问题描述】:

我正在处理ReversibleContainer 及其关联的LegacyRandomAccessIterators。它们包装了一个预先存在的数据结构,该结构表示可直接索引的对象集合。我决定让迭代器独立存在,并让默认构造的迭代器代表“结束”:

// constructs an iterator that provides a view of 'data'
the_iterator (thedata *data, difference_type index = 0);

// constructs an iterator representing the end
the_iterator ();

所以我可以这样做例如:

std::for_each(the_iterator(data), the_iterator(), ...);

迭代器完成所有工作。容器非常轻巧。我像这样实现了容器的begin()end()

struct the_container {
    the_data *data; // <- object wrapped by this container
    the_iterator begin () { return the_iterator(data); }
    the_iterator end () { return the_iterator(); }
};

我已经让它工作得很好,但在测试中我意识到我搞砸了,它不符合基本的Container 要求,因为:

  • 事实证明,对于具有双向迭代器的容器,end() is required to return a decrementable iterator 当容器非空时,但
  • 我的默认构造的结束迭代器不存储关于任何thedata 的任何信息,因此它不能被递减,因为它不知道任何特定集合的“最后一个元素”是什么。李>

所以我现在必须修复容器。我正在考虑的解决方案是(让data-&gt;number_of_items 包含项目数):

  • 继续允许默认构造的迭代器表示“结束”。
  • 还让the_iterator(data, data-&gt;number_of_items)代表“结束”,符合容器要求。这个迭代器是可递减的。

那么容器会这样做:

struct the_container {
    the_data *data; // <- object wrapped by this container
    the_iterator begin () { return the_iterator(data, 0); }
    the_iterator end () { return the_iterator(data, data->number_of_items); }
};

现在,这很好,它满足所有 Container 要求。但是,我现在想知道是否允许我的默认构造的迭代器存在。

那么,我的问题是:虽然 Container 对从 end() 返回的迭代器提出了递减要求,但对于仅代表“结束”的迭代器是否有类似的要求? "的一些数据但不涉及容器的end()

更正式地说,如果:

  • j 是一个双向迭代器
  • container.empty() == false
  • ( j == container.end() ) == true

那么--j 是否需要有效并且需要最终指向容器的最后一个元素?就我而言,这种情况的一个例子是:

the_container container(data); // <- assume data->number_of_items > 0
the_iterator b = container.begin();
the_iterator e = container.end();
the_iterator j;

assert(container.empty() == false);
assert(e == j);
assert(distance(b, e) == distance(b, j));

-- e;  // <- this is required to be well-defined
-- j;  // <- but is this??

所以,是的,这是我的问题。我担心可能会在例如&lt;algorithm&gt; 可能会假设我的“结束”迭代器之一是可递减的,或者我正在破坏一些我不理解的微妙内容。

【问题讨论】:

  • 我希望这个问题有意义;很难用语言表达。

标签: c++ iterator containers c++17 language-lawyer


【解决方案1】:

作为一个框架挑战,你在这里拥有的是一个哨兵。哨兵是可以与迭代器进行比较的东西,但它不是迭代器。

激励示例是一种类型,当与 char 指针比较时,它只检查 char 指针的取消引用是否为空。您可以使用它来编写 C 级“读取字符串直到 null”,同时仍然使用 for(:) 循环等。

基于范围的for(:) 循环得到增强,允许开始和结束具有不同的类型,从而允许哨兵存在并最大限度地提高效率。

考虑用结束哨兵替换您的默认构造迭代器技巧。这不适用于许多 pre-ranges-v2 算法,但至少您正在使用经过验证的真实模式。

不,我不认为您的最终迭代器违反了规则。例如,来自两个容器的迭代器在标准下可能会产生无意义的== 结果。您的最终迭代器可以被视为来自另一个容器的迭代器,具有可预测的 == 行为。

现在,当你将它传递给算法时会发生什么?这可能会使您的程序格式错误;该算法可以检测到您为特征提供的保证,并且您的假结束迭代器不是有效的双向迭代器,与开始迭代器位于同一容器中。

【讨论】:

  • 我从没想过两个不同的容器。这本身就很有说服力。我查看了C++17 draft 并发现,在 [forward.iterators] (§27.2.5), ¶2 中,== 的前向迭代器的域是相同的迭代器顺序。然后,它指出所有值初始化的迭代器必须相互比较相等,并且表现得像序列结束迭代器一样指向相同的空序列(意味着彼此相同,但不同于任何其他序列)。所以无论如何,它在== 的域之外。
  • (注意:之前的某个地方,我忘了在哪里,有一个定义指出“初始化的值”包括默认构造的。)
【解决方案2】:

那么--j是否需要有效并且需要最终指向容器的最后一个元素?

对于任何双向迭代器,只要满足其先决条件,--j 就必须明确定义。前提是存在另一个迭代器s,其j == ++s。对于您的容器,最后一个有效元素的迭代器就是这样的迭代器

因此,我不相信您的迭代器会满足双向迭代器的要求,因此容器将不可逆。

如果您想为双向迭代器定义哨兵,可以通过使用单独的类型来完成,这样迭代器的要求将不适用于它。需要注意的是,大多数标准算法不支持单独的迭代器和哨兵类型。 std::ranges 算法可以。

或者,您可以指定默认的初始化迭代器是 singular,即它不与任何容器关联。在这种情况下,-- j 没有任何要求(j == e 也没有)。但是,b, j 将不是有效范围,您仍然不能将这样的对传递给标准算法。

【讨论】:

  • 所以,换句话说,只要end()返回那个默认构造的迭代器(不满足双向要求),那么容器就不能满足可逆要求。因此,虽然我可以修复 end() 以返回令人满意的迭代器(从而修复损坏的可逆容器),但如果我将独立的无容器默认构造迭代器传递给算法并且需要参数为双向迭代器。不过,Otoh 至少在只需要前向迭代器时我还可以。对吗?
  • @JasonC 只要你正确指定迭代器不是双向迭代器。需要一个迭代器类别的算法可能会根据给定迭代器的类别专门化其行为。
猜你喜欢
  • 2012-08-16
  • 2011-07-16
  • 1970-01-01
  • 2021-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-21
  • 2021-01-27
相关资源
最近更新 更多