【问题标题】:polymorphic iterators in C++C++中的多态迭代器
【发布时间】:2011-01-31 15:32:54
【问题描述】:

我正在尝试在 C++ 中实现多态迭代器。基本上,我需要它才能应用过滤器,以便迭代器根据相关条件跳过一些项目。所以我创建了一个带有抽象接口的GoF-like 迭代器,这允许我从中派生一个过滤的迭代器并实现所需的逻辑。我也更喜欢基于接口的迭代器而不是模板迭代器,因为它们允许隐藏实现而不会导致混乱的鸭子类型模板。

但是,多态迭代器不能按值返回(与 STL 迭代器相反),所以我必须传递指针,这很容易变得像这种情况一样危险,这似乎合乎逻辑但会导致内存泄漏:

Iter* Collection::GetIter() {...} // new IterImpl
DoSomething(Iter*) {...} // doesn't do delete

DoSomething(Collection.GetIter()); // convenient, but wrong :\

显而易见的解决方案是使用某种智能指针来控制迭代器的生命周期,但人们常说接口应该尽可能简单和通用,所以应该避免智能指针?

如果您在 C++ 中使用过多态迭代器,这个问题是如何解决的?还是基于模板的迭代器是 C++ 中唯一“好”的迭代方式?谢谢。

【问题讨论】:

  • 您可以很容易地在 C++ 中实现过滤迭代器,而不会暴露任何外部多态性(Boost 提供 a generic filter iterator)。
  • @7vies:Matthie M 对该问题的回答使用了智能指针,但作为实现细节,不在接口中。所以我认为你对简单/通用接口的反对并不适用。顺便说一句,如果您更喜欢多态迭代器而不是使用模板进行鸭式打字,我可以推荐 Java 吗? ;-p
  • @7vies:它存在,但是 STL 强制执行了非常严格的迭代器模型,与 GOF 模型完全不兼容:/
  • @7vies:如果你想要mark-sweep GC,那么我可以推荐Java吗?或者你可以试试 Boehm 的 C++ GC。 C++ 迭代器是用值语义设计的,Matthieu 对另一个问题的回答是在值语义和运行时多态性之间架起桥梁的“正确”习语。在这种情况下,智能指针完全按照需要管理分配的资源。我真的不知道“跛脚”应该是什么意思——智能指针可以很好地处理这种情况。那么有什么问题呢?
  • @7vies:“如果我不打算在容器上使用 STL 算法,为什么还要对容器使用 STL 迭代器?”——您不必使用 STL 迭代器。但是你说你要按值返回,这意味着你必须实现值语义。顺便说一句,这意味着使用shared_ptr 而不是unique_ptr,这样它是可复制的,如果你从马蒂厄的回答开始的话。

标签: c++ iterator polymorphism


【解决方案1】:

通常的做法是使用编译时多态而不是运行时多态;这使编译器有更多机会使用迭代器优化代码,并且通常在现代 C++ 中更惯用。

如果您确实需要运行时多态行为,那么将多态性封装在迭代器本身中而不将其暴露在外部可能是最简单的。您可以使用诸如function 之类的多态函数包装器来完成此操作,该包装器可在 Boost、C++ TR1 和 C++0x 中找到。我在这里提供了一个基于我的一个爱好项目中的过滤器迭代器的示例:

template <typename ForwardIt>
class filter_iterator
    : public std::iterator<
          std::forward_iterator_tag, 
          typename std::iterator_traits<ForwardIt>::value_type>

{
public:

    typedef typename std::iterator_traits<ForwardIt>::value_type ValueType;
    typedef typename std::function<bool(ValueType)> FunctionType;

    filter_iterator() { }

    explicit filter_iterator(ForwardIt end)
        : it_(end), end_(end) 
    {
    }

    filter_iterator(ForwardIt it, ForwardIt end, FunctionType is_filtered) 
        : it_(it), end_(end), is_filtered_(is_filtered)
    { 
        skip_filtered_elements(); 
    }

    const ValueType& operator*()  const { return it_.operator*();  }
    const ValueType* operator->() const { return it_.operator->(); }

    filter_iterator& operator++()   
    { 
        ++it_; skip_filtered_elements(); return *this; 
    }

    filter_iterator operator++(int) 
    { 
        filter_iterator it(*this); ++*this; return it; 
    }


    friend bool operator==(const filter_iterator& lhs,
                           const filter_iterator& rhs)
    {
        return lhs.it_ == rhs.it_;
    }

    friend bool operator!=(const filter_iterator& lhs,
                           const filter_iterator& rhs)
    {
        return !(lhs == rhs);
    }

private:

    void skip_filtered_elements()
    {
        while (it_ != end_ && is_filtered_(*it_))
            std::advance(it_, 1);
    }

    ForwardIt it_;
    ForwardIt end_;

    std::function<bool(const ValueType&)> is_filtered_;
};

template <typename ForwardIt>
filter_iterator<ForwardIt> make_filter_iterator(ForwardIt end)
{
    return filter_iterator<ForwardIt>(end);
}

template <typename ForwardIt, typename Function>
filter_iterator<ForwardIt> make_filter_iterator(ForwardIt it, 
                                                ForwardIt end, 
                                                Function f)
{
    return filter_iterator<ForwardIt>(it, end, f);
}

使用很简单。此示例(使用 C++0x lambda 表达式作为函数类型)演示了从范围中过滤奇数:

int main()
{
    std::array<int, 4> x = { 1, 2, 3, 4 };

    std::copy(make_filter_iterator(x.begin(), x.end(), [](int i) { return i % 2; }),
              make_filter_iterator(x.end()),
              std::ostream_iterator<int>(std::cout, " "));
}

【讨论】:

  • 你不觉得我们需要“两个”迭代器来实现任何“智能”行为很笨拙吗?我觉得 Stepanov 将“指针”概念与“迭代器”概念混淆了,并且必须传递两个项目才能安全地进行迭代,这会导致接口混乱:/
  • 仅当“智能”行为涉及跳过元素时。
  • @Matthieu:是的,它很笨重。在我当前的项目中,我在一个地方编写了五个过滤器、转换和累积迭代器,不得不引入一堆变量来让代码可读,这让我很难过(不过,我没有任何运行时多态性;只是很多很多的编译时多态性)。
  • @James McNellis:我也担心你“不关心”的东西会成为一个大项目的瓶颈——模板(至少在它们当前的实现中)会导致快速增长编译依赖,例如N*M 个案例,用于 M 种类型的 N 个算法。使用运行时多态性,只有 N 种情况应用于可以由 M 种类型实现的接口。并且没有实现细节。
  • @James McNellis:我猜如果有超过 100 万行代码,顶层代码不是基于模板的,是吗?你还有一些非模板化的抽象,还是“一切都是模板”?我只是不明白你是如何设法阻止“模板雪球”的,这样它就不需要包含 1M 行代码来编译单个表达式?
【解决方案2】:

这里有两个问题:

  • 语法:STL 假定迭代器提供需要与实际项目匹配的特征(例如value_typereference)。
  • 语义:迭代器应该是可复制的。

请记住(在 C++ 中)迭代器不是范围,因此 ++ 操作很快就会变得混乱,因为您需要跳过一些项目,但是(使用传统实现)您无法知道有多少项目随时为您服务...

因此,如果您想要遵循 GOF 接口的多态迭代器,您将不得不放弃使用 STL 算法。

也就是说,实现多态迭代器是完全可行的:

struct IterBase
{
  virtual void increment() = 0;
  virtual void decrement() = 0;

  // others
};

class Iter
{
public:
  Iter& operator++() { base->increment(); return *this; }
  Iter operator++(int) { Iter tmp(*this); base->increment(); return tmp; }

  // others

private:
  std::unique_ptr<IterBase> base;
};

然后你需要编写所有的复制构造函数、赋值运算符和析构函数来做正确的事情......

不过,如果没有模板多态性,那么只有当您的迭代器仅用于同一类型时才值得...

【讨论】:

  • GoF 迭代器知道终止条件(例如IsDone)。它们不兼容 STL,但我真的不明白为什么需要在它们上运行 STL 算法。不幸的是,我没有 unique_ptr (C++03) ;( 但我可以尝试模拟它。但是,我仍然不知道这是否真的比仅使用智能指针更好,因为这在定义上更复杂而不是一个智能指针——你有一个成员,你不能隐藏它的实现,因为它是基于模板的,你唯一可以“隐藏”的是它的接口。
  • 您可以在 C++03 中使用普通的scoped_ptr(因为您不需要移动语义)。与“单个”智能指针相比,主要优势在于您可以提供一组减少的操作来覆盖,然后在 Iter 类中以多种方式组合它们以提供“真实”接口(即,可由.)。至于基于模板的智能指针......我认为你有偏见;)
  • @Matthieu M.:我有什么偏见? :) 如果你是这个意思,我不讨厌 模板。我只提到这个选项不是智能指针的替代品,而是更像是智能指针之上的包装器,因此它暗示了与智能指针相关的所有可能问题,而不是更少。
  • @7vies:它暗示了智能指针的技术问题......但隐藏了功能性问题(因为它们被包装了)。例如,我不知道scoped_ptr 有哪些技术问题。无论如何,如果您愿意,您可以随时重新编码智能指针逻辑。 Pimpl 成语不需要使用它们,它只是简化了使用它们的任务,因为它们有效。
  • @Matthieu M.:scoped_ptr 的一个可能的“技术”问题是您无法移动它,只能复制,而有时您实际上想移动它。我不确定限制对下面使用的实际智能指针的访问有什么好处,除非这种访问以某种方式干扰了迭代器本身的功能......并且通过“重新编码”智能指针 - 你的意思是创建我自己的智能指针,而不是使用现有的精心设计和测试的?
【解决方案3】:

可以使用包含某种形式的指针的迭代器来完成,然后将功能传递给指针。

虽然这样做你需要非常小心,而且我已经多次看到它做错了(包括我自己曾经爱过它,然后想知道为什么测试失败了......)

  • 不要使用 shared_ptr!

幸运的是,我没有看到上面有人犯了使用 shared_ptr 的错误,但它有错误的语义,因为当你复制一个迭代器时,你有 2 个单独的副本。但是,如果它们包含一个 shared_ptr 并且您推进其中一个迭代器,另一个将随之移动 - 意外行为...

因此,每次复制时都需要进行克隆,但幸运的是,使用 C++0x,您可以在大部分时间移动而不是克隆。

您还需要知道,每次迭代的操作都会命中 v-table,这可能会导致它的运行速度比将“宏”方法设为多态(并且可能使用模板实现,因此您不需要重写代码)。

【讨论】:

  • 感谢您提出 shared_ptr 问题,很高兴能记住这一点。
【解决方案4】:

我在 Oli Charlesworth 的问题上看到了一个很好的解决方案,但并没有得到太多的赞誉(至少,没有我想的那么多)。

class Iterator
{
public:
    SmartPointer<IteratorImplementation> ItrPtr;

    //Delegate methods to ItrPtr
}

然后您可以通过值传递迭代器并将方法延迟到包含的智能指针;它基本上是一个实现“策略”模式的迭代器,策略表现出多态行为。

【讨论】:

  • 看起来像粉刺。 pimpl 的丑陋之处在于“委托方法”部分......它与智能指针完全一样。
  • @7vies:是的,但用户不知道他们正在处理一个智能指针,只是一个迭代器。对我来说似乎比仅仅将智能指针传回迭代器更干净......
  • @7vies:其实不一样。我在下面自己的答案中添加了更多上下文,但问题是迭代器在它们提供的方法中有很多冗余(例如 ++ 的变化),因此您实际上可以减少要覆盖的方法数量IterImpl 类的实现。
【解决方案5】:

人们确实说界面应该尽可能简单和通用。在您的情况下,您将原始指针描述为“不可能”的东西。因此,我建议您使用智能指针的明显解决方案是可能的最简单和通用的技术。

为了使这个智能指针尽可能简单和通用,我会使用 stl 提供的智能指针之一,因为它们是最普遍的。

【讨论】:

    【解决方案6】:

    去过那里。做到了。

    您可以做的是将您的迭代器接口隐藏在另一个迭代器后面。 假设您有几种迭代器,它们都隐藏在 IIterator 接口后面。

    然后编写另一个类似迭代器的类,例如MyIterator,它包含一个指向 IIterator 的指针,它只是将所有调用转发给 IIterator,如下所示:

    template <typename T>
    class MyIterator
        {
        public:
           MyIterator() : m_iterator(nullptr) {}
           MyIterator(IIterator *it) : m_iterator(it) {}
           MyIterator &operator++()
              {
              if (m_iterator) m_iterator->operator++();
              return *this;
              }
           T &operator*() const
              {
              if (m_iterator) return m_iterator->operator*();
              else            throw an exception?
              }
        private
           IIterator *m_iterator;
        };
    

    这个例子远未完成,但你应该明白了。

    【讨论】:

    • 它必须以某种方式管理生命周期,所以它始终是相同的智能指针解决方案......
    猜你喜欢
    • 2011-05-13
    • 2019-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-30
    • 2013-01-11
    • 2015-10-05
    • 2015-10-04
    相关资源
    最近更新 更多