Zezwól na iterację wektora wewnętrznego bez wycierania implementacji

32

Mam klasę reprezentującą listę osób.

class AddressBook
{
public:
  AddressBook();

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

Chcę pozwolić klientom na iterację po wektorze ludzi. Pierwszą myślą, którą miałem, było po prostu:

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

Nie chcę jednak ujawniać klientowi szczegółów implementacji . Mogę chcieć zachować pewne niezmienniki, gdy wektor jest modyfikowany, i tracę kontrolę nad tymi niezmiennikami, gdy wyciekam z implementacji.

Jaki jest najlepszy sposób na iterację bez wycieku elementów wewnętrznych?

Eleganckie prace przygotowawcze
źródło
2
Przede wszystkim, jeśli chcesz zachować kontrolę, powinieneś zwrócić wektor jako stałe odniesienie. W ten sposób nadal ujawniasz szczegóły implementacji, więc zalecam, aby twoja klasa była iterowalna i nigdy nie ujawniała struktury danych (może jutro będzie to tablica skrótów?).
idoby,
Szybkie wyszukiwanie w Google ujawniło mi ten przykład: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown
1
To, co mówi @DocBrown, jest prawdopodobnie właściwym rozwiązaniem - w praktyce oznacza to, że dajesz klasie AddressBook metodę begin () i end () (plus przeciążenia const, a ostatecznie także cbegin / cend), które po prostu zwracają początek wektora () i koniec ( ). W ten sposób twoja klasa będzie również użyteczna we wszystkich najbardziej standardowych algorytmach.
stijn
1
@stijn To powinna być odpowiedź, a nie komentarz :-)
Philip Kendall
1
@stijn Nie, nie tak mówi DocBrown i powiązany artykuł. Prawidłowym rozwiązaniem jest użycie klasy proxy wskazującej klasę kontenera wraz z bezpiecznym mechanizmem wskazującym pozycję. Zwracanie wektora begin()i end()jest niebezpieczne, ponieważ (1) te typy są iteratorami (klasami) wektorów, które uniemożliwiają przejście do innego kontenera, takiego jak a set. (2) Jeśli wektor zostanie zmodyfikowany (np. Wyhodowany lub niektóre elementy usunięte), niektóre lub wszystkie iteratory wektora mogły zostać unieważnione.
rwong

Odpowiedzi:

25

zezwolenie na iterację bez wycieku elementów wewnętrznych jest dokładnie tym, co obiecuje wzór iteratora. Oczywiście jest to głównie teoria, więc oto praktyczny przykład:

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

Podajesz standard begini endmetody, podobnie jak sekwencje w STL i implementujesz je po prostu przekazując do metody wektorowej. To przecieka niektóre szczegóły implementacji, a mianowicie, że zwracasz iterator wektorowy, ale żaden rozsądny klient nigdy nie powinien na tym polegać, więc nie jest to problemem. Pokazałem tutaj wszystkie przeciążenia, ale oczywiście możesz zacząć od podania stałej wersji, jeśli klienci nie będą mogli zmieniać żadnych wpisów People. Korzystanie ze standardowego nazewnictwa ma zalety: każdy, kto czyta kod, od razu wie, że zapewnia „standardową” iterację i jako taki działa ze wszystkimi popularnymi algorytmami, zasięgiem opartym na pętlach itp.

stijn
źródło
uwaga: chociaż to z pewnością działa i jest akceptowane, warto zwrócić uwagę na komentarze rwong do pytania: dodanie dodatkowego otoki / proxy wokół iteratorów wektora uniezależniłoby klientów od rzeczywistego iteratora
stijn
Ponadto należy zauważyć, że udostępnienie znaku „a” begin()i end()to tylko do przodu do wektora begin()i end()pozwala użytkownikowi modyfikować elementy w samym wektorze, być może przy użyciu std::sort(). W zależności od niezmienników, które próbujesz zachować, może to być akceptowalne. Zapewnienie begin()i end(), choć, jest konieczne do obsługi pętli w zakresie C ++ 11.
Patrick Niedzielski,
Prawdopodobnie powinieneś również pokazać ten sam kod, używając auto jako zwracanych typów funkcji iteratora, gdy używasz C ++ 14.
Klaim,
Jak to ukrywa szczegóły implementacji?
BЈовић
@ BЈовић, nie ujawniając pełnego wektora - ukrywanie niekoniecznie oznacza, że ​​implementacja musi być dosłownie ukryta przed nagłówkiem i umieszczona w pliku źródłowym: jeśli to prywatny klient i tak nie ma dostępu
stijn
4

Jeśli iteracja jest wszystko, czego potrzeba, to może owinięcie wokół std::for_eachwystarczy:

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;
};
Kocia kołyska
źródło
Prawdopodobnie lepiej byłoby wymusić stałą przy pomocy cbegin / cend. Ale to rozwiązanie jest zdecydowanie lepsze niż zapewnienie dostępu do podstawowego kontenera.
galop1n
@ galop1n To ma wymusić constiteracji. Jest for_each()to constfunkcja członka. W związku z tym członek peoplejest postrzegany jako const. Stąd begin()i end()będzie przeciążać jak const. Dlatego powrócą const_iteratordo people. W związku z f()tym otrzyma People const&. Pisanie cbegin()/ cend()tutaj nic nie zmieni, w praktyce, chociaż jako obsesyjny użytkownik, constmogę argumentować, że nadal warto to robić, ponieważ (a) dlaczego nie; to tylko 2 znaki, (b) lubię mówić, co mam na myśli, przynajmniej z const, (c) chroni przed przypadkowym wklejeniem gdzie indziej const, itp.
underscore_d
3

Możesz użyć idiomu pimpl i podać metody iteracji po kontenerze.

W nagłówku:

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

W źródle:

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

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

W ten sposób, jeśli twój klient użyje typedef z nagłówka, nie zauważy, jakiego rodzaju kontenera używasz. A szczegóły implementacji są całkowicie ukryte.

BЈовић
źródło
1
Jest to PRAWIDŁOWE ... całkowite ukrywanie implementacji i brak dodatkowych kosztów ogólnych.
Abstrakcja jest wszystkim.
2
@Abstrakcjaiseverything. „ brak dodatkowych kosztów ogólnych ” jest po prostu fałszywe. PImpl dodaje dynamiczny przydział pamięci (a później wolny) dla każdej instancji oraz wskaźnik pośredni (przynajmniej 1) dla każdej metody, która przez nią przechodzi. To, czy jest to narzut duży w danej sytuacji, zależy od testów porównawczych / profilowania, a w wielu przypadkach prawdopodobnie jest całkowicie w porządku, ale absolutnie nie jest prawdą - i myślę raczej nieodpowiedzialnie - ogłaszanie, że nie ma narzutu.
underscore_d
@underscore_d Zgadzam się; nie chciałem być nieodpowiedzialny, ale wydaje mi się, że padłem ofiarą kontekstu. „Bez dodatkowych kosztów ogólnych ...” jest technicznie niepoprawny, jak zręcznie zauważyłeś; przeprosiny ...
Abstrakcja jest wszystkim.
1

Można zapewnić funkcje członkowskie:

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

Które umożliwiają dostęp bez ujawniania szczegółów implementacji (takich jak ciągłość) i wykorzystują je w klasie iteratora:

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

Iteratory mogą być następnie zwrócone przez książkę adresową w następujący sposób:

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

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

Prawdopodobnie będziesz musiał uzupełnić klasę iteratora o cechy itp., Ale myślę, że spełni to, o co prosiłeś.

jbcoe
źródło
1

jeśli chcesz dokładnej implementacji funkcji ze std :: vector, użyj prywatnego dziedziczenia jak poniżej i kontroluj, co jest widoczne.

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

Edycja: Nie jest to zalecane, jeśli chcesz również ukryć wewnętrzną strukturę danych, tj. Std :: vector

Ayub
źródło
Dziedziczenie w takiej sytuacji jest co najwyżej bardzo leniwe (powinieneś użyć kompozycji i podać metody przekazywania, zwłaszcza, że ​​jest tu tak mało do przekazania), często mylące i niewygodne (co jeśli chcesz dodać własne metody, które są w konflikcie z nimi vector, którego nigdy nie chcesz używać, ale mimo to musisz odziedziczyć?), a może być aktywnie niebezpieczny (co jeśli leniwie odziedziczona klasa mogłaby zostać usunięta przez wskaźnik do tego typu bazy gdzieś, ale [nieodpowiedzialnie] nie chroniła przed zniszczeniem wyprowadzony obiekt za pomocą takiego wskaźnika, więc po prostu zniszczenie go to UB?)
underscore_d