允许内部向量的迭代而不会泄漏实现


32

我有一堂课代表一个人的名单。

class AddressBook
{
public:
  AddressBook();

private:
  std::vector<People> people;
}

我想允许客户遍历人脉。我首先想到的只是:

std::vector<People> & getPeople { return people; }

但是,我不想将实现细节泄漏给客户端。修改向量时,我可能想保留某些不变式,而当我泄漏实现时,我将失去对这些不变式的控制。

允许迭代而不泄漏内部的最佳方法是什么?


2
首先,如果要保持控制,则应将向量作为const引用返回。您仍然会以这种方式公开实现细节,因此我建议使您的类可迭代,并且从不公开您的数据结构(也许明天将成为哈希表?)。
idoby 2014年

谷歌快速搜索显示了以下示例:sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown

1
@DocBrown所说的可能是适当的解决方案-在实践中,这意味着您为AddressBook类提供了begin()和end()方法(加上const重载,最后还有cbegin / cend),它们仅返回向量的begin()和end( )。这样一来,您的课程也将可用于所有大多数标准算法。
stijn 2014年

1
@stijn应该是一个答案,而不是评论:-)
Philip Kendall

1
@stijn不,那不是DocBrown和链接的文章所说的。正确的解决方案是使用指向容器类的代理类以及指示位置的安全机制。返回向量的begin()end()是很危险的,因为(1)这些类型是向量迭代器(类),它阻止一个向量切换到另一个容器,例如set。(2)如果修改了向量(例如,增长了或删除了某些项目),则某些或所有向量迭代器可能已经失效。
rwong 2014年

Answers:


25

允许迭代而不会泄漏内部元素正是迭代器模式所承诺的。当然这主要是理论,因此这里是一个实际示例:

class AddressBook
{
  using peoples_t = std::vector<People>;
public:
  using iterator = peoples_t::iterator;
  using const_iterator = peoples_t::const_iterator;

  AddressBook();

  iterator begin() { return people.begin(); }
  iterator end() { return people.end(); }
  const_iterator begin() const { return people.begin(); }
  const_iterator end() const { return people.end(); }
  const_iterator cbegin() const { return people.cbegin(); }
  const_iterator cend() const { return people.cend(); }

private:
  peoples_t people;
};

您提供标准beginend方法,就像STL中的序列一样,只需转发到vector的方法即可实现它们。这确实泄漏了一些实现细节,即您将返回向量迭代器,但任何理智的客户端都不应依赖于此,因此imo不必担心。我在这里显示了所有重载,但是如果客户端不应该更改任何People条目,那么当然可以从提供const版本开始。使用标准命名有好处:任何阅读代码的人都立即知道它提供了“标准”迭代,因此可以与所有通用算法,基于循环的范围等一起使用。


注意:尽管这确实可行并且被接受,但值得注意rwong对这个问题的评论:在vector的迭代器周围添加额外的包装器/代理将使客户独立于实际的底层迭代器
stijn 2014年

此外,请注意,提供begin()end()只是转发给向量,begin()并且end()允许用户使用修改向量本身中的元素std::sort()。根据您要保留的不变式,这可能会接受也可能不会接受。但是,提供begin()end()是支持基于C ++ 11范围的for循环所必需的。
Patrick Niedzielski 2014年

使用C ++ 14时,可能还应该使用auto作为迭代器函数的返回类型来显示相同​​的代码。
Klaim 2014年

这如何隐藏实施细节?
BЈовић

@BЈовић不公开完整的向量- 隐藏并不一定意味着必须从标头中隐藏实现并将其放在源文件中:如果私有客户端仍然无法访问它
stijn 2014年

4

如果只需要迭代,那么包装std::for_each就足够了:

class AddressBook
{
public:
  AddressBook();

  template <class F>
  void for_each(F f) const
  {
    std::for_each(begin(people), end(people), f);
  }

private:
  std::vector<People> people;
};

用cbegin / cend强制执行const迭代可能会更好。但是,到目前为止,该解决方案比提供对底层容器的访问要好得多。
galop1n 2014年

@ galop1n它确实强制执行const迭代。的for_each()是一个const成员函数。因此,该成员people被视为const。因此,begin()end()将重载为const。因此,它们将返回const_iteratorpeople。因此,f()将收到一个People const&。在实践中,写cbegin()/ cend()在这里不会改变任何东西,尽管作为一个const我的痴迷用户,我可能仍然认为值得这样做,因为(a)为什么不这样做;它只有2个字符,(b)我想说我的意思,至少是const,(c)防止意外粘贴非const
underscore_d

3

您可以使用pimpl惯用语,并提供迭代容器的方法。

在标题中:

typedef People* PeopleIt;

class AddressBook
{
public:
  AddressBook();


  PeopleIt begin();
  PeopleIt begin() const;
  PeopleIt end();
  PeopleIt end() const;

private:
  struct Imp;
  std::unique_ptr<Imp> pimpl;
};

在源中:

struct AddressBook::Imp
{
  std::vector<People> people;
};

PeopleIt AddressBook::begin()
{
  return &pimpl->people[0];
}

这样,如果您的客户端使用标题中的typedef,他们将不会注意到您正在使用哪种容器。并且实施细节被完全隐藏。


1
这是正确的...完整的实现隐藏,没有额外的开销。
抽象就是一切。

2
@Abstractioniseverything。”没有额外的开销 ”显然是错误的。PImpl为每个实例添加了动态内存分配(后来又释放了空间),并为通过它的每个方法添加了指针间接寻址(至少为1)。对于任何给定情况而言,这是否会产生大量开销都取决于基准测试/性能分析,并且在许多情况下,这可能是完全可以的,但是宣称它没有开销绝对是不正确的-我认为这是不负责任的-我认为这是不负责任的。
underscore_d

@underscore_d我同意;并不是说在那里不负责任,而是,我猜想我是这种情况的牺牲品。正如您巧妙指出的那样,“没有额外的开销...”在技术上是不正确的;道歉...
抽象就是一切。

1

一个可以提供成员函数:

size_t Count() const
People& Get(size_t i)

它允许访问而不会暴露实现细节(例如连续性),并在迭代器类中使用这些细节:

class Iterator
{
    AddressBook* addressBook_;
    size_t index_;

public:
    Iterator(AddressBook& addressBook, size_t index=0) 
    : addressBook_(&addressBook), index_(index) {}

    People& operator*()
    {
        return addressBook_->Get(index_);
    }

    Iterator& operator ++ ()
    {
       ++index_;
       return *this;
    }

    bool operator != (const Iterator& i) const
    {
        assert(addressBook_ == i.addressBook_);
        return index_ != i.index_;
    }
};

然后,地址簿可以返回迭代器,如下所示:

AddressBook::Iterator AddressBook::begin()
{
    return Iterator(this);
}

AddressBook::Iterator AddressBook::end()
{
    return Iterator(this, Count());
}

您可能需要用特质等丰富迭代器类,但是我认为这可以满足您的要求。


1

如果要从std :: vector精确实现功能,请按如下所示使用私有继承并控制公开的内容。

template <typename T>
class myvec : private std::vector<T>
{
public:
    using std::vector<T>::begin;
    using std::vector<T>::end;
    using std::vector<T>::push_back;
};

编辑:如果您还想隐藏内部数据结构(即std :: vector),则不建议这样做


在这种情况下,继承充其量是非常懒惰的(您应该使用组合并提供转发方法,尤其是因为此处转发的人很少),通常会造成混乱和不便(如果您想添加自己的与之冲突的vector方法,它永远不会使用,但是仍然必须继承?),并且可能非常危险(如果懒惰继承的类可以通过指向某个基本类型的指针在某个地方删除的方式来删除,但它[不负责任]不能防止破坏通过这样的指针派生的obj,因此只需销毁它就是UB?)
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.