【问题标题】:How to allow derived classes to return any iterator over a type through a virtual method?如何允许派生类通过虚拟方法返回任何类型的迭代器?
【发布时间】:2017-04-18 00:10:39
【问题描述】:

我正在尝试编写一个允许用户实现某些行为的小库,以便该库可以使用他自己的底层数据结构。在这种特殊情况下(尽管我正在简化),我试图定义如下内容:

class LibraryClass {
    virtual std::vector<int>::const_iterator intsBegin() const = 0;
    virtual std::vector<int>::const_iterator intsEnd() const = 0;
};

换句话说,库将接受使用任何允许它迭代整数向量的派生类。现在棘手的一点是:我不想强制使用向量。真的,任何整数迭代器对我来说都是可以的,只要我能检测到它何时结束,并将其取消引用为整数。这是缺少的部分:

class LibraryClass {
    virtual /* ??? */ intsBegin() const = 0;
    virtual /* ??? */ intsEnd() const = 0;
};

class UserClass {
    /* This should be allowed, here I'm using vector as an example. */
    std::vector<int>::const_iterator intsBegin() const;
    std::vector<int>::const_iterator intsEnd() const;
};

现在,我知道如何通过模板为方法参数设置that kind of behaviour,但我找不到在虚拟方法中将其用于返回值的方法,因为:

  • 迭代器是否为const 不是很重要。
  • 基类中不会有任何默认行为。
  • 该机制不能被继承破坏:派生自int 迭代器的类也应该被接受为返回类型,因为它们满足要求。我不认为这是一个问题,因为传递性,但仍然。
  • 理想情况下,beginend 方法应强制在给定派生类中具有相同的迭代器类型。
  • 更理想的是,我希望避免将 Boost 用于 enable_if 之类的事情。该库是使用 C++11 编写的(至少我尝试过)。

【问题讨论】:

  • 性能重要吗?如果是这样,基于每个元素的类型擦除迭代是个坏主意。
  • boost::any_iterator

标签: c++11 templates iterator return-value virtual-functions


【解决方案1】:

不如提供一个界面,让用户对数据做任何他们想做的事,但不使用迭代器?

class LibraryClass {
public:
    virtual void visitInts(std::function<void(int)> f) = 0;
};

class UserClass : public LibraryClass {
public:
    void visitInts(std::function<void(int)> f) override {
        for (int data : m_vec)
            f(data);
    }
private:
    std::vector<int> m_vec;
};

您失去了std::binary_search 的能力,但您大多拥有相同的多功能性,而无需在UserClass 上强制使用特定的存储机制。

【讨论】:

  • 感谢您的回答!遗憾的是,一般的步行并不是我所需要的:在我的情况下,f 对整数无能为力。库检索它们以进行进一步处理,这些东西不能(干净)包含在LibraryClassUserClass 中,也不是用户定义的。
  • @JohnWHSmith 为什么不呢?传入[&amp;](int x){ vec.push_back(x); }
【解决方案2】:

您也可以将迭代器设为虚拟类,例如(未经测试,显然不完整):

class LibraryClass {
protected:
  struct IteratorBase { // user needs to implement this:
     virtual const int& deref() const = 0;
     virtual void increment() = 0;
     virtual void decrement() = 0;
     // further methods, e.g., comparison and maybe cloning
  };
  virtual std::unique_ptr<IteratorBase> intsBegin_() const = 0;
  virtual std::unique_ptr<IteratorBase> intsEnd_() const = 0;
public:
  class Iterator {
    friend class LibraryClass;
    std::unique_ptr<IteratorBase> iter;
    Iterator(std::unique_ptr<IteratorBase>&& iter_) : iter(std::move(iter_) {}
  public:
    const int& operator*() { return iter->deref(); }
    const Iterator& operator++() { iter->increment(); return *this; }
    // further operators ...
  };
  Iterator intsBegin() const { return Iterator(intsBegin_()); }
  Iterator intsEnd() const   { return Iterator(intsEnd_(); }
};

我不确定这是否值得付出努力,尤其是因为即使对于非常简单的任务,您也会获得很多间接性。我想你应该重新考虑你的设计......

【讨论】:

  • 我确实有一个改变设计的替代方案,但它使整个事情的互操作性降低(我正在编写代码来连接其他库,这些库并不能很好地发挥作用)。我想我可以定义一个迭代器接口并使用运算符重载使其相当透明;它实际上可能用于其他目的。在我接受答案之前,我会再等一会儿。
  • 另一种选择当然是静态多态性(即基本上模板化所有内容)——但您似乎不希望那样。
【解决方案3】:

任何迭代器都不难编写。如果您关心性能,这通常是个坏主意,因为每个操作都需要非零开销来执行类型擦除。

假设你只需要支持for(:)循环和“输入”迭代。

namespace any_iteration_support {
  template<class VTable, class...Ts>
  VTable create() {
    VTable retval;
    VTable::template populate<Ts...>(retval);
    return retval;
  }
  template<class VTable, class...Ts>
  VTable const* get_vtable() {
    static VTable vtable = create<VTable, Ts...>();
    return &vtable;
  }
  struct input_iterator_vtable {
    void(*next)(void*) = 0;
    void(*dtor)(void*) = 0;
    void(*copy)(void* dest, void const* src) = 0;
    void*(*clone)(void const* src) = 0;
    bool(*equals)(void const* lhs, void const* rhs) = 0;
    template<class It>
    static void populate( input_iterator_vtable& vtable ) {
      vtable.next = [](void* ptr){
        auto& it = *static_cast<It*>(ptr);
        ++it;
      };
      vtable.dtor= [](void* ptr){
        auto* pit = static_cast<It*>(ptr);
        delete pit;
      };
      vtable.copy= [](void* dest, void const* src){
        auto& destit = *static_cast<It*>(dest);
        auto const& srcit = *static_cast<It const*>(src);
        destit = srcit;
      };
      vtable.clone= [](void const* src)->void*{
        auto const& srcit = *static_cast<It const*>(src);
        return new It(srcit);
      };
      vtable.equals= [](void const* lhs, void const* rhs)->bool{
        auto const& lhsit = *static_cast<It const*>(lhs);
        auto const& rhsit = *static_cast<It const*>(rhs);
        return lhsit == rhsit;
      };
    }
  };
  template<class T>
  struct any_input_iterator_vtable:input_iterator_vtable {
    T(*get)(void*) = 0;
    template<class It>
    static void populate(any_input_iterator_vtable& vtable) {
      input_iterator_vtable::populate<It>(vtable);
      vtable.get = [](void* ptr)->T{
        auto& it = *static_cast<It*>(ptr);
        return *it;
      };
    }
  };
}
template<class T>
struct any_input_iterator {
  any_iteration_support::any_input_iterator_vtable<T> const* vtable = 0;
  void* state = 0;
  template<class It,
    std::enable_if_t<!std::is_same<It, any_input_iterator>::value, int> =0
  >
  any_input_iterator( It it ):
    vtable(any_iteration_support::get_vtable<any_iteration_support::any_input_iterator_vtable<T>, It>()),
    state( new It(std::move(it)) )
  {}
  any_input_iterator(any_input_iterator&& o):
    vtable(o.vtable),
    state(o.state)
  {
    o.vtable = 0; o.state = 0;
  }
  any_input_iterator& operator=(any_input_iterator&& o) {
    using std::swap;
    swap(vtable, o.vtable);
    swap(state, o.state);
    return *this;
  }
  any_input_iterator(any_input_iterator const& o) {
    if (!o.vtable) return;
    state = o.vtable->clone(o.state);
    vtable = o.vtable;
  }
  any_input_iterator& operator=(any_input_iterator const& o) {
    if (!vtable && !o.vtable) return *this;
    if (vtable == o.vtable) {
      vtable->copy( state, o.state );
      return *this;
    }
    clear();
    if (!o.vtable) {
      return *this;
    }
    state = o.vtable->clone(o.state);
    vtable = o.vtable;
    return *this;
  }
  explicit operator bool()const { return vtable; }
  friend bool operator==(any_input_iterator const& lhs, any_input_iterator const& rhs) {
    if (!lhs && !rhs) return true;
    if (!lhs) return false;
    if (lhs.vtable != rhs.vtable) return false;
    return lhs.vtable->equals( lhs.state, rhs.state );
  }
  friend bool operator!=(any_input_iterator const& lhs, any_input_iterator const& rhs) {
    return !(lhs==rhs);
  }
  T operator*() const {
    return vtable->get( state );
  }
  any_input_iterator& operator++() {
    vtable->next(state);
    return *this;
  }
  any_input_iterator operator++(int) {
    auto retval = *this;
    ++*this;
    return retval;
  }
  ~any_input_iterator() { clear(); }
  void clear() {
      if (!vtable) return;
      auto dtor = vtable->dtor;
      auto s = state;
      state = 0;
      vtable = 0;
      dtor(s);
  }
  using reference = T;
  using value_type = typename std::remove_reference<T>::type;
  using iterator_category = std::input_iterator_tag;
};

这是一个关于如何在类型T 上消除输入迭代概念的快速草图。很像std::function,它几乎可以存储任何满足输入迭代要求的东西。

boost 提供了类似的类型。

Live example.

通常最好编写一个函数,每次调用一次访问多个节点,甚至访问一个节点,以减少类型擦除开销。迭代器很快,因为它们可以内联;在每个方法上都遵循一个 void 指针,它们可能会非常缓慢。

class LibraryClass {
public:
  virtual any_input_iterator<const int> intsBegin() const = 0;
  virtual any_input_iterator<const int> intsEnd() const = 0;
};

当然,手动执行这种类型的擦除既乏味又容易出错。我会使用类型擦除类型擦除之类的东西,而不是上面的。

有效的方法是使用跨度访问者

template<class T>
struct span {
  T* b = 0; T* e = 0;
  T* begin() const { return b; }
  T* end() const { return e; }
  bool empty() const { return begin()==end(); }
  std::size_t size() const { return end()-begin(); }
  span()=default;
  span(span const&)=default;
  span& operator=(span const&)=default;
  span(T* s, T* f):b(s),e(f) {};
  span(T* s, std::size_t length):span(s, s+length) {}
};
template<class T>
using span_visitor = std::function<void(span<T>)>;

class LibraryClass {
public:
  virtual void visitInts(span_visitor<int const> f) const = 0;
};

class VecImpl : public LibraryClass {
public:
  virtual void visitInts(span_visitor<int const> f) const override {
    f( {m_vec.data(), m_vec.size()} );
  };
private:
  std::vector<int> m_vec;
};
class SimpleListImpl : public LibraryClass {
public:
  virtual void visitInts(span_visitor<int const> f) const override {
    for (int i : m_list )
      f( {&i, 1} );
  };
private:
  std::list<int> m_list;
};
class FancyListImpl : public LibraryClass {
public:
  virtual void visitInts(span_visitor<int const> f) const override {
    std::size_t count = 0;
    std::array<int, 10> buffer;
    for (int i : m_list ) {
      buffer[count] = i;
      ++count;
      if (count == buffer.size()) {
        f({ buffer.data(), buffer.size()});
        count = 0;
      }
    }
    if (count) f({ buffer.data(), count});
  };
private:
  std::list<int> m_list;
};

你是怎么吃的?

std::vector<int> get_ints_from(LibraryClass const& library) {
  std::vector<int> vec;
  library.visitInits([&](span<int const> ints){
    vec.append(ints.begin(), ints.end());
  });
  return vec;
}

此接口将类型擦除从每个元素多次删除到每个多个元素一次。

使用这种方法,您可以获得接近直接公开容器的性能,但从不真正公开它。

我还没有编译 span 版本。

【讨论】:

    猜你喜欢
    • 2016-06-22
    • 1970-01-01
    • 1970-01-01
    • 2011-06-26
    • 2015-06-21
    • 1970-01-01
    • 2010-11-22
    • 1970-01-01
    • 2016-04-09
    相关资源
    最近更新 更多