Ermöglichen Sie die Iteration eines internen Vektors, ohne die Implementierung zu verlieren

32

Ich habe eine Klasse, die eine Liste von Personen darstellt.

class AddressBook
{
public:
  AddressBook();

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

Ich möchte Kunden erlauben, über den Vektor von Menschen zu iterieren. Der erste Gedanke, den ich hatte, war einfach:

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

Allerdings möchte ich nicht die Details der Implementierung an den Client lecken . Ich möchte möglicherweise bestimmte Invarianten beibehalten, wenn der Vektor geändert wird, und ich verliere die Kontrolle über diese Invarianten, wenn ich die Implementierung verliere.

Was ist der beste Weg, um die Iteration zuzulassen, ohne die Interna zu verlieren?

Elegantes Codeworks
quelle
2
Wenn Sie die Kontrolle behalten möchten, sollten Sie zunächst Ihren Vektor als konstante Referenz zurückgeben. Sie würden weiterhin Implementierungsdetails auf diese Weise verfügbar machen. Ich empfehle daher, Ihre Klasse iterativ zu gestalten und niemals Ihre Datenstruktur bereitzustellen (möglicherweise handelt es sich morgen um eine Hash-Tabelle?).
idoby
Eine schnelle Google-Suche ergab folgendes Beispiel: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown
1
Was @DocBrown sagt, ist wahrscheinlich die richtige Lösung. In der Praxis bedeutet dies, dass Sie Ihrer AddressBook-Klasse eine begin () - und end () -Methode (plus const-Überladungen und schließlich auch cbegin / cend) geben, die einfach den begin () und den end () des Vektors zurückgibt. ). Auf diese Weise kann Ihre Klasse auch von allen gängigen Algorithmen verwendet werden.
6.
1
@stijn Das sollte eine Antwort sein, kein Kommentar :-)
Philip Kendall
1
@stijn Nein, das steht nicht in DocBrown und dem verlinkten Artikel. Die richtige Lösung besteht darin, eine Proxy-Klasse zu verwenden, die auf die Containerklasse zeigt, sowie einen sicheren Mechanismus zum Anzeigen der Position. Das Zurückgeben der Vektoren begin()und end()ist gefährlich, da (1) diese Typen Vektoriteratoren (Klassen) sind, die verhindern, dass einer zu einem anderen Container wie a wechselt set. (2) Wenn der Vektor modifiziert wird (z. B. gewachsen oder einige Elemente gelöscht), könnten einige oder alle Vektoriteratoren ungültig gemacht worden sein.
rwong

Antworten:

25

Iteration zuzulassen, ohne die Interna zu verlieren, ist genau das, was das Iterationsmuster verspricht. Das ist natürlich hauptsächlich Theorie. Hier ist ein praktisches Beispiel:

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

Sie stellen Standards beginund endMethoden wie Sequenzen in der AWL zur Verfügung und implementieren sie einfach durch Weiterleitung an die Methode von vector. Hierdurch werden einige Implementierungsdetails preisgegeben, nämlich, dass Sie einen Vektoriterator zurückgeben, aber kein vernünftiger Client sollte sich jemals darauf verlassen, sodass dies kein Problem darstellt. Ich habe hier alle Überladungen angezeigt, aber natürlich können Sie zunächst nur die const-Version bereitstellen, wenn Clients keine People-Einträge ändern können sollen. Die Verwendung der Standardbenennung hat Vorteile: Jeder, der den Code liest, weiß sofort, dass er eine "Standard" -Iteration bietet und als solche mit allen gängigen Algorithmen, auf Schleifen basierenden Bereichen usw. funktioniert.

stijn
quelle
Anmerkung: Obwohl dies sicherlich funktioniert und akzeptiert wird, lohnt es sich, die Kommentare von rwong zu der Frage zu beachten: Das Hinzufügen eines zusätzlichen Wrappers / Proxys um die Iteratoren von vector würde Clients unabhängig vom eigentlichen zugrunde liegenden Iterator machen
am
Beachten Sie außerdem, dass Sie ein begin()und end()dieses nur an den Vektor weiterleiten begin()und end()es dem Benutzer ermöglichen, die Elemente im Vektor selbst zu ändern, möglicherweise mithilfe von std::sort(). Abhängig davon, welche Invarianten Sie beibehalten möchten, kann dies akzeptabel sein oder nicht. Es ist jedoch erforderlich, C ++ 11-bereichsbasierte for-Schleifen bereitzustellen begin()und end()zu unterstützen.
Patrick Niedzielski
Wenn Sie C ++ 14 verwenden, sollten Sie wahrscheinlich auch denselben Code mit auto als Rückgabetyp für Iteratorfunktionen anzeigen.
Klaim
Wie versteckt dies die Implementierungsdetails?
BЈовић
@ BЈовић nicht den kompletten Vektor verfügbar machen - Verstecken bedeutet nicht zwangsläufig, dass die Implementierung buchstäblich vor einem Header verborgen und in die Quelldatei
eingefügt
4

Wenn Sie nur eine Iteration benötigen, ist möglicherweise ein Wrapper std::for_eachausreichend:

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;
};
Katzenkrippe
quelle
Es wäre wahrscheinlich besser, eine Konstituierung mit cbegin / cend zu erzwingen. Diese Lösung ist jedoch weitaus besser, als Zugriff auf den zugrunde liegenden Container zu gewähren.
galop1n
@ galop1n Es tut eine erzwingen constIteration. Das for_each()ist eine constMitgliedsfunktion. Daher wird das Mitglied peopleals gesehen const. Daher begin()und end()wird als überladen const. Daher kehren sie zu const_iterators zurück people. Daher f()wird ein erhalten People const&. Das Schreiben cbegin()/ cend()hier wird in der Praxis nichts ändern, obwohl constich als obsessiver Benutzer der Ansicht sein könnte, dass es sich immer noch lohnt, dies zu tun, als (a) warum nicht; es sind nur 2 Zeichen, (b) ich sage gerne, was ich meine, zumindest mit const, (c) es schützt vor versehentlichem Einfügen an einer anderen Stelle constusw.
underscore_d
3

Sie können das Pimpl-Idiom verwenden und Methoden bereitstellen, um den Container zu durchlaufen .

In der Kopfzeile:

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

In der Quelle:

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

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

Auf diese Weise merkt Ihr Client nicht, welche Art von Container Sie verwenden, wenn er das typedef aus dem Header verwendet. Und die Implementierungsdetails sind vollständig verborgen.

BЈовић
quelle
1
Dies ist RICHTIG ... vollständige Implementierung versteckt und kein zusätzlicher Aufwand.
Abstraktion ist alles.
2
@Abstraktionist alles. " kein zusätzlicher Aufwand " ist eindeutig falsch. PImpl fügt für jede Instanz eine dynamische (und später freie) Speicherzuweisung und für jede Methode, die dies durchläuft, eine Zeiger-Indirektion (mindestens 1) hinzu. Ob das für eine bestimmte Situation viel Overhead bedeutet, hängt vom Benchmarking / Profiling ab, und in vielen Fällen ist es wahrscheinlich vollkommen in Ordnung, aber es ist absolut nicht wahr - und ich halte es für ziemlich verantwortungslos - zu behaupten, dass es keinen Overhead gibt.
Underscore_d
@underscore_d Ich stimme zu; Das bedeutet nicht, dort unverantwortlich zu sein, aber ich schätze, dass ich dem Kontext zum Opfer gefallen bin. "Kein zusätzlicher Aufwand ..." ist technisch inkorrekt, wie Sie gekonnt betont haben. Entschuldigung ...
Abstraktion ist alles.
1

Man könnte Mitgliedsfunktionen bereitstellen:

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

Die den Zugriff ermöglichen, ohne Implementierungsdetails (wie die Kontiguität) offenzulegen, und diese innerhalb einer Iteratorklasse verwenden:

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

Iteratoren können dann wie folgt vom Adressbuch zurückgegeben werden:

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

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

Wahrscheinlich müssten Sie die Iteratorklasse mit Merkmalen usw. ausstatten, aber ich denke, dass dies tun wird, was Sie gefragt haben.

jbcoe
quelle
1

Wenn Sie die Funktionen von std :: vector genau implementieren möchten, verwenden Sie die folgende private Vererbung und steuern Sie, was verfügbar gemacht wird.

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

Bearbeiten: Dies wird nicht empfohlen, wenn Sie auch die interne Datenstruktur, dh std :: vector, ausblenden möchten

Ayub
quelle
Vererbung in einer solchen Situation ist bestenfalls sehr faul (Sie sollten Komposition verwenden und Weiterleitungsmethoden bereitstellen, zumal es hier so wenige Weiterleitungsmethoden gibt), oft verwirrend und unpraktisch (was ist, wenn Sie Ihre eigenen Methoden hinzufügen möchten, die mit vectordenen in Konflikt stehen , die Sie nie verwenden möchten, aber dennoch erben müssen?) und möglicherweise aktiv gefährlich (was ist, wenn die Klasse, von der Sie träge erben, durch einen Zeiger auf diesen Basistyp irgendwo gelöscht wird, aber [unverantwortlicherweise] nicht vor der Zerstörung von geschützt ist?) Ein abgeleitetes Objekt über einen solchen Zeiger, also einfach zerstören ist UB?)
underscore_d