如何实现符合标准的迭代器/容器

How to implement an iterator/container that is standards-compliant?

本文关键字:标准 迭代器 容器 何实现 实现      更新时间:2023-10-16

假设我有一个名为RuleBook的抽象容器类。RuleBook的用户希望能够对RuleBook进行前向迭代以获得Rule

与标准容器不同,此处对具体子类的内存布局没有限制。 相反,子类的实现者需要遵守RuleBook的前向迭代要求,并根据其自身的数据结构满足此要求。

我认为RuleBook应该包含纯虚拟begin()end(),以便它可以与基于范围的 for一起使用,但我遇到了一些问题。

begin() 和 end() 的签名应该是什么? 篮球规则和公司规则应该如何实施?

当迭代器经过最后一项时,我们如何处理结束条件?

在下面的示例中,您可以假设m_rpm_rpp分别指向一个规则。 我们想为胆量返回某种迭代器(如Rule*)。 我们还可以假设Foo的所有子类都将在各种数据结构中包含Rule,这将取决于实现者的心血来潮。

如果我使用Rule*作为迭代器,null_ptr作为我的超越端点来实现整个事情,这是否符合 STL 标准?

我目前正在研究自定义迭代器,但我什至不确定这个问题是否适合该范式,因为每个子类都必须有效地定义迭代的内脏。

法典

struct RuleBook
{
// virtual Rule* begin() = 0;
// virtual Rule* end() = 0; 
};
struct CompanyRules :
public RuleBook
{
Rule m_r;
};
struct BasketballRules :
public RuleBook
{
// return the three Rules, one-at-a-time, in succession
Rule   m_r;
Rule*  m_rp;
Rule** m_rpp;
};
int
main( int argv, char* argc[] )
{
}

这很难做到正确。

begin() 和 end() 的签名应该是什么?

没有太多选择,他们几乎必须是类似的东西

RuleBook::iterator begin();
RuleBook::iterator end();

(如果需要,可以添加const重载)

篮球规则和公司规则应该如何实施?

仔细:)

当迭代器经过最后一项时,我们如何处理结束条件?

您可以正确设计迭代器类型,使其正常工作。 您需要一个可以比较相等性并且可以递增的迭代器类型。当您有一个迭代器到容器中的最后一个项目并递增它时,它必须等于过去结束的迭代器。

如果我使用 Rule* 作为我的迭代器,null_ptr作为我的超越端点来实现整个事情,这是否符合 STL 标准?

不。 如果您的迭代器类型只是Rule*那么递增迭代器不会移动到下一个Rule,它只是指向内存中的下一个位置,该位置甚至可能不是Rule对象,从而导致未定义的行为。 例如,对于BasketballRules,如果您有指向m_rRule*并且您递增它,那么您不是指向一个有效的Rule对象,而是指向m_rp占用的内存,即Rule*并取消引用它是未定义的行为。

此外,如果您不断递增Rule*则永远不会达到过去的结束nullptr值。

我给Yakk的回答投了赞成票,因为它是一个合理的实现,但很难做到正确。 多态接口中需要考虑和包含很多事情,例如,如果您使用==比较两个RuleBook::iterator对象,其中一个指向CompanyRules,一个指向BasketballRules,会发生什么,相等性如何适用于多态迭代器?

如果将迭代器分配给BasketballRules对象,将迭代器分配给CompanyRules对象,会发生什么情况?您需要多态类型的"深拷贝"。

每个容器都需要不同的派生迭代器-impl 类型,CompanyRule容器的迭代器类型需要了解有关CompanyRule类型的所有信息,依此类推每个派生容器类型。这些具体的迭代器-impl 类型中的每一个都需要将几乎整个迭代器接口实现为虚函数。 实现它的难度表明设计存在问题。

更简单的设计是针对每个派生容器类型管理相同类型的实际物理容器。 特定于每个派生容器的代码只包括每当派生对象的内容更改时更新列表的内容。然后,迭代器类型是简单和非多态的,例如

struct RuleBook
{
typedef std::vector<Rule*> list_type;
typedef list_type::iterator iterator;
virtual iterator begin() = 0;
virtual iterator end() = 0;
};
struct CompanyRules :
public RuleBook
{
CompanyRules() : m_list{ &m_r } { }
Rule m_r;
iterator begin() { return m_list.begin(); }
iterator end() { return m_list.end(); }
private:
list_type m_list;
};
struct BasketballRules :
public RuleBook
{
BaseketballRules() : m_list{ &m_r, m_rp, *m_rpp } { }
// return the three Rules, one-at-a-time, in succession
Rule   m_r;
Rule*  m_rp;
Rule** m_rpp;
iterator begin() { return m_list.begin(); }
iterator end() { return m_list.end(); }
private:
// N.B.
// must update m_list[1] any time m_rp changes
// must update m_list[2] any time the pointee of m_rpp changes (harder!)
list_type m_list;
};

确切的签名无关紧要,因为基于范围的 for 循环是根据所使用的表达式定义的。如果找到beginend成员函数,则像__range.begin()__range.end()一样调用它们。签名无关紧要的一个例子是,这些成员函数可以具有任意数量和类型的参数,只要它们可以像.begin().end()一样被调用(这意味着参数必须具有默认值)。

如果类型没有beginend成员函数,则使用表达式begin(__range)end(__range)。(其中__rangeauto &&__range使用您在 range-for-loop 中使用的表达式进行初始化)。因此,只要参数相关查找有效,确切的签名就很重要。


篮球规则和公司规则应该如何实施?

当迭代器经过最后一项时,我们如何处理结束条件?

这些是与范围基础循环如何工作不同的问题。您应该专门询问有关这些的其他问题。

但是,如果您使用指针作为迭代器,那么使用nullptr作为结束迭代器是不合适的,因为增加最后一个有效指针不会给你一个空指针;它会给你一个超过范围末尾的指针。此外,如果您使用Rule*作为迭代器,那么您不会将实现留给容器类;容器必须维护一个连续的规则数组。

Boost 有一些迭代器帮助程序模板类 - crtp,需要你实现一些方法。 基于pImpl的用户将允许迭代器的合规和虚拟行为。 也就是说,pImpl是一个纯粹的虚拟抽象类,迭代器将其工作委托给它。

编写一个纯虚拟pImpl迭代器。 这就是返回类型beginend. 子类使用具体的pImpl实例创建迭代器的实例,这些实例是为存储数据的方式自定义编写的。

也许你可以尝试像Alex Allain在他的教程中解释的关于基于范围的for循环一样。它还有一个示例,用于创建可通过基于范围的 for 循环迭代的数据结构

需要的东西是

  1. 一个 .begin() 和一个 .end() 方法。它们也可以是独立的。
  2. 运算符 != 、++ 和 * 重载,以便支持需要执行的迭代器操作。