【问题标题】:How to return a generic iterator (independent of particular container)?如何返回通用迭代器(独立于特定容器)?
【发布时间】:2011-01-19 04:51:11
【问题描述】:

我想设计一个类Foo,它存储不同类型的各种数据并返回对它们的迭代器。它应该是通用的,所以Foo 的用户不知道数据是如何存储的(Foo 可能正在使用std::setstd::vector 或其他)。

我很想写一个这样的界面:

class Foo {
  class FooImpl;
  FooImpl* impl_;
public:
  const Iterator<std::string>& GetStrings() const;
  const Iterator<int>& GetInts() const;
};

Iterator 是这样的(就像 .NET 中的迭代器):

template<class T>
class Iterator {
public:
  const T& Value() const = 0;
  bool Done() const = 0;
  void Next() = 0;
};

但我知道这种迭代器在 C++ 中不是标准的,最好像 STL 那样使用迭代器,这样你就可以对它们使用 STL 算法。

我该怎么做? (我需要iterator_traits吗?)

【问题讨论】:

标签: c++ stl iterator pimpl-idiom


【解决方案1】:

你明白为什么 STL 选择将迭代器实现细节放在头文件中吗? JIT 框架能够跨编译单元内联,但 C++ 只能在编译单元内内联。内联时通过序列的前进速度要快得多,函数调用的成本在实际遍历数据结构时占主导地位。

如果您真的想隐藏实现细节,请继续。您可以创建一个兼容 STL 的迭代器,根据受保护的虚函数实现 operator++ 和 operator!= 和 operator->,您提到的 Next、Done 和 Value 将是不错的名称。只是期望为性能较低的封装付费。

【讨论】:

  • 这种迭代器是 OP 所熟悉的,但我认为他问的是如何编写一个遵循 C++ stdlib 约定的迭代器,而不是如何打破这些约定。
  • 将 operator++、operator!= 和 operator-> 委托给受保护的虚函数的哪一部分不清楚?公共用户界面就是操作符,就像 C++ 标准库中的容器一样。
  • 我的意思是您在第一段中指出的影响性能的约定。 “但我知道……最好像 STL 那样使用迭代器……我该怎么做?”
【解决方案2】:

具有迭代器的 c++ 类必须提供至少两个函数,如果它们必须与 std 库一起使用

iterator begin() //returns an iterator at starting pos
iterator end() //returns an iterator one past end or just invald

迭代器必须重载自增运算符,equals 和 *

iterator operator++()
iterator operator==()//make sure that an invalid iterator equals end()
T& operator*()

您可以使用迭代器类来包裹内部存储的迭代器,以确保用户仅限于这些方法。

template <typename T> iter
{
   iter(T::iterator& intern)
   T::value_type& operator*(){return *intern}
  iter operator++(){return iter(++intern);}
  bool operator==(iter const& other)const{return intern == other.intern;}
}

其中 T 是您的容器的类型。(该类不完整,我可能混淆了一些东西)

【讨论】:

  • 简洁地解释迭代器基础知识的好尝试,但在这种情况下需要更多信息,例如sgi.com/tech/stl/Iterators.html
  • @Roger Pate 我认为通用访问的基础知识已经足够了,据我所知,它与发布的 .Net 版本相同。我可能误解了这个问题(英语是我的第二语言)
  • 您已经描述了前向迭代器,这是一些语言所拥有的。在 C++ 迭代器中有相当多的微妙之处。名为 begin/end 的方法本身并不是必需的,因为 stdlib 从不调用它们,但您确实需要支持 STL 样式的迭代器范围才能处理诸如 之类的东西。我不会认为您的答案是错误的,但对于我的口味来说,这过于简单化了,我认为链接到更完整的参考是合适的。
【解决方案3】:

看起来您正在尝试创建独立于容器的代码,这(通常)不是一个好主意,除非您正在编写一个可以单独使用迭代器操作的算法。 (参见 Scott Myers Effective STL Item 2:当心容器独立代码的错觉)

问题在于大多数标准容器不提供重叠功能。如果您正在为特定容器编写代码,请假设您正在为该容器编写代码。不要费心让它独立于容器。

【讨论】:

  • 感谢 Scott Myer 项目的链接。
【解决方案4】:

使用 typedef 返回boost::iterator_range。例如(不管名字),

class Container
{
     typedef std::vector<int> Collection; 

     public:
     typedef boost::iterator_range<Collection::iterator> CollectionRange;
     typedef Collection::iterator CollectionIterator;
     Range range() const {
          return make_iterator_range(collection_.begin(), collection_.end());
     }

     private:
     Collection collection_;          
};

用户代码将是

Container c;
// ...
FOREACH(int i, c.range()) { //... }
Container::Range r = c.range();
for(Container::iterator j = r.begin(); j!= r.end(); j++) { // ... }

这不是通用的,但同样的想法可以用于模板。

【讨论】:

  • 很好,但这仍然暴露了头文件中的std::vector&lt;int&gt;。我认为vector 的使用是一个实现细节,理想情况下我想将其隐藏在实现文件中。
  • 我猜你将不得不在容器上使用 pimpl。
  • @dehmann:隐藏你包含 是一个实际问题还是只是一个理论上的问题?记录他们应该在你的类中使用 typedef,然后保留它。无论如何,任何愿意忽略您的指示并继续阅读代码以查找实现细节的人都会给您带来比这更大的问题。
  • @roger:是的,这更多是理论上的问题。我希望能够“插入任何实现”,而无需更改接口。但也许我会放弃那个理想。
  • @dehmann:请记住,您应该能够将标头更改为“仅插入”另一个实现并拥有一切 Just Work(tm)。 (例如,将 typedef 从“vector”更改为“deque”或“something_else”。)这是因为 STL 样式使用嵌套的迭代器 typedef 等等。您将不得不重新编译,因为这确实会更改您的类的二进制接口,但不会更改源代码接口
【解决方案5】:

为了满足特定容器(向量、集合、...)在头文件中未提及的要求,并且用户将能够遍历所有字符串,使用 访问者模式。当然,缺点是用户不能在字符串上使用 STL 算法。

// foo.h
class StringVisitor {
public:
  void accept(const std::string& str) {
    std::cout << str << std::endl;
  }
};
class Foo {
  class Impl;
  Impl* impl_;
public:
  Foo();
  ~Foo();
  void VisitStrings(StringVisitor v) const;
};

// foo.cc
class Foo::Impl {
  typedef std::vector<std::string> StringContainer;
  StringContainer str_;
public:
  Impl() {
    str_.push_back("a");
    str_.push_back("b");
  }
  void VisitStrings(StringVisitor v) const {
    for(StringContainer::const_iterator it = str_.begin();
    it != str_.end(); ++it){
      v.accept(*it);
    }
  }  
};

Foo::Foo() : impl_(new Impl()) {}
Foo::~Foo() {delete impl_;}
void Foo::VisitStrings(StringVisitor v) const {
  impl_->VisitStrings(v);
}

// main.cc
int main() {
  Foo foo;
  foo.VisitStrings(StringVisitor());
  return 0;
}

【讨论】:

  • 你为什么要回答你自己的问题(而且这么快,太快了)以一种似乎完全不同的方向?看起来你似乎是在尝试用这个问题来支持某种争论的观点然后回答。
  • 嗯,我一直在思考,并为这些特定要求找到了替代解决方案。它不适用于 STL 算法,但在此处记录它作为访问类内部容器的替代方法仍然很好。
  • 有什么特定要求? (将它们放入问题中可能会有所帮助——我所看到的只是一些叙述,然后询问如何编写 STL 风格的迭代器。)
  • @roger:要求是能够迭代数据,但是 Foo 接口不会暴露它存储数据的方式。我认为这个问题很清楚,而我的回答清楚地表明它满足了这些要求?
  • @dehmann:这与我关于理论与实践的其他评论有关。在struct S { typedef int* iterator; }; 中,它“公开”它使用指针(指向读取头文件的任何人),但要求您使用 S::iterator 类型定义,这样就不需要类的用户知道它使用指针。它对指针的使用是封装的,因为我可以更改 typedef 并且正确编写的任何内容都将继续工作;这样指针就暴露了。
猜你喜欢
  • 2017-02-10
  • 1970-01-01
  • 2021-05-09
  • 1970-01-01
  • 1970-01-01
  • 2013-01-18
  • 1970-01-01
  • 2021-12-09
  • 2014-02-02
相关资源
最近更新 更多