実装をリークすることなく、内部ベクトルの反復を許可します


32

人々のリストを表すクラスがあります。

class AddressBook
{
public:
  AddressBook();

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

クライアントが人々のベクトルを反復処理できるようにします。私が持っていた最初の考えは単純でした:

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

しかしながら、 実装の詳細をクライアントに漏らしたくありません。ベクトルが変更されたときに特定の不変式を維持したい場合があり、実装をリークするとこれらの不変式に対する制御が失われます。

内部をリークすることなく反復を許可する最良の方法は何ですか?


2
まず、制御を維持したい場合は、ベクトルをconst参照として返す必要があります。それでも実装の詳細をそのように公開するので、クラスを反復可能にし、データ構造を公開しないことをお勧めします(明日はハッシュテーブルになるのでしょうか?)。
idoby 14

簡単なグーグル検索でこの例がわかりました
Doc Brown 14

1
@DocBrownが言うことはおそらく適切な解決策です-実際には、これはAddressBookクラスにbegin()およびend()メソッド(さらにconstオーバーロード、最終的にはcbegin / cendも)を与え、単にベクターのbegin()およびend( )。そうすることで、クラスはすべてのほとんどのstdアルゴリズムで使用可能になります。
stijn 14

1
@stijnコメントではなく答えである必要があります:
フィリップケンドール14

1
@stijnいいえ、DocBrownとリンクされた記事はそうではありません。正しい解決策は、位置を示すための安全なメカニズムとともに、コンテナクラスを指すプロキシクラスを使用することです。(1)これらの型はaのような別のコンテナへの切り替えを防ぐベクトル反復子(クラス)であるため、ベクトルを返すbegin()end()危険setです。(2)ベクターが変更された場合(たとえば、成長または一部のアイテムが消去された場合)、ベクターイテレーターの一部またはすべてが無効化された可能性があります。
rwong

回答:


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;
};

STLのシーケンスと同じように、標準beginendメソッドを提供し、ベクターのメソッドに転送するだけでそれらを実装します。これはいくつかの実装の詳細を漏らします。つまり、ベクトルイテレータを返しますが、健全なクライアントがそれに依存することはないので、気にする必要はありません。ここではすべてのオーバーロードを示しましたが、もちろん、クライアントがPeopleエントリを変更できないようにする必要がある場合は、constバージョンを指定するだけで開始できます。標準のネーミングを使用することには利点があります。コードを読んでいる人はすぐに「標準」の反復を提供することを知っているため、すべての一般的なアルゴリズム、範囲ベースのループなどで動作します。


注:これは確かに動作し、それは質問にのコメントをrwongの撮影ノートの価値が認められているものの:ここでは、余分なラッパー/プロキシの周りのベクトルのイテレータを追加すると、実際の根本的なイテレータのクライアントが独立してになるだろう
stijn

また、提供することを注記begin()してend()ベクトルのにちょうど前方のことをbegin()してend()、おそらく使用して、ユーザーがベクター自体の要素を変更することができますstd::sort()。保存しようとしている不変条件に応じて、これは受け入れられる場合と受け入れられない場合があります。ただし、C ++ 11の範囲ベースのforループをサポートするには、begin()とを提供するend()必要があります。
パトリックニージエルスキ14

また、C ++ 14を使用する場合、おそらく反復関数の戻り値の型としてautoを使用して同じコードを表示する必要があります。
クライム14

これは実装の詳細をどのように隠していますか?
BЈовић

@BЈовић完全なベクトルを暴露しないことによって- 隠れが必ずしも実装は文字通りヘッダから隠され、ソースファイルに置く必要があるという意味ではありません:それはプライベートなクライアントだ場合はとにかくそれをアクセスすることはできません
stijn

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 14

galop1n @それはありません強制const反復を。for_each()あるconstメンバ関数。したがって、メンバーpeopleはと見なされconstます。したがって、begin()およびend()としてオーバーロードされconstます。したがって、彼らが返されますconst_iteratorに秒people。したがって、f()を受け取りますPeople const&。ライティングcbegin()/ cend()の強迫観念ユーザーとしてけれども、ここでは、実際には、何も変わらないだろうconst、私、それはまだやって価値があると主張する理由はない()として、; それは、少なくともで、私が何を意味するかのように言ってちょうど2文字、(b)はIをだconst(c)は、それは偶然どこか非貼り付けてからデバイスを保護し、constなど、
underscore_d

3

pimpl idiomを使用て、コンテナーを反復処理するメソッドを提供できます。

ヘッダー内:

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、あなたは決して使用したくないが、それでも継承する必要がありますか?)、そして積極的に危険かもしれませんそのようなポインタを介して派生したオブジェクト、それで単にそれを破壊するのはUBですか?)
underscore_d
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.