C ++ Iterator, Warum gibt es keine Iterator-Basisklasse, von der alle Iteratoren erben?

11

Ich lerne für eine Prüfung und habe eine Frage, die ich nur schwer geben und beantworten kann.

Warum gibt es keine Iterator-Basisklasse, von der alle anderen Iteratoren erben?

Ich vermute, mein Lehrer bezieht sich auf die hierarchische Struktur aus der CPP-Referenz " http://prntscr.com/mgj542 ", und wir müssen einen anderen Grund angeben, als warum sollten sie?

Ich weiß, was Iteratoren sind (irgendwie) und dass sie verwendet werden, um an Containern zu arbeiten. Soweit ich weiß, haben unterschiedliche Container aufgrund der unterschiedlichen möglichen zugrunde liegenden Datenstrukturen unterschiedliche Iteratoren, da Sie beispielsweise zufällig auf ein Array zugreifen können, jedoch nicht auf eine verknüpfte Liste, und unterschiedliche Container unterschiedliche Arten des Durchlaufs erfordern.

Es handelt sich wahrscheinlich um spezialisierte Vorlagen, je nach Container, oder?

jnt
quelle
2
Das Teilen Ihrer Forschung hilft allen . Sagen Sie uns, was Sie versucht haben und warum es Ihren Anforderungen nicht entsprach. Dies zeigt, dass Sie sich die Zeit genommen haben, um sich selbst zu helfen. Es erspart uns, offensichtliche Antworten zu wiederholen, und vor allem hilft es Ihnen, eine spezifischere und relevantere Antwort zu erhalten. Siehe auch Fragen
Mücke
5
" Warum gibt es keine Iterator-Basisklasse, von der alle anderen Iteratoren erben? " Ähm ... warum sollte es eine geben?
Nicol Bolas

Antworten:

14

Sie haben bereits Antworten erhalten, die darauf hinweisen, warum nicht alle Iteratoren von einer einzelnen Iterator-Basisklasse erben müssen. Ich war allerdings noch ein Stück weiter gekommen. Eines der Ziele von C ++ ist die Abstraktion ohne Laufzeitkosten.

Wenn Iteratoren von allen arbeiten, die von einer gemeinsamen Basisklasse erben, und virtuelle Funktionen in der Basisklasse verwenden, um die Schnittstelle zu definieren, und die abgeleiteten Klassen Implementierungen dieser virtuellen Funktionen bereitstellen, könnte (und würde dies häufig) eine erhebliche Laufzeit hinzufügen Overhead für die beteiligten Operationen.

Betrachten wir zum Beispiel eine einfache Iteratorhierarchie, die Vererbung und virtuelle Funktionen verwendet:

template <class T>
class iterator_base { 
public:
    virtual T &operator*() = 0;
    virtual iterator_base &operator++() = 0;
    virtual bool operator==(iterator_base const &other) { return pos == other.pos; }
    virtual bool operator!=(iterator_base const &other) { return pos != other.pos; }
    iterator_base(T *pos) : pos(pos) {}
protected:
    T *pos;
};

template <class T>
class array_iterator : public iterator_base<T> {
public: 
    virtual T &operator*() override { return *pos; }
    virtual array_iterator &operator++() override { ++pos; return *this; }
    array_iterator(T *pos) : iterator_base(pos) {}
};

Dann lassen Sie es uns kurz testen:

int main() { 
    char input[] = "asdfasdfasdfasdfasdfasdfasdfadsfasdqwerqwerqwerqrwertytyuiyuoiiuoThis is a stringy to search for something";
    using namespace std::chrono;

    auto start1 = high_resolution_clock::now();
    auto pos = std::find(std::begin(input), std::end(input), 'g');
    auto stop1 = high_resolution_clock::now();

    std::cout << *++pos << "\n";

    auto start2 = high_resolution_clock::now();
    auto pos2 = std::find(array_iterator(input), array_iterator(input+sizeof(input)), 'g');
    auto stop2 = high_resolution_clock::now();

    std::cout << *++pos2 << "\n";

    std::cout << "time1: " << duration_cast<nanoseconds>(stop1 - start1).count() << "ns\n";
    std::cout << "time2: " << duration_cast<nanoseconds>(stop2 - start2).count() << "ns\n";
}

[Hinweis: Abhängig von Ihrem Compiler müssen Sie möglicherweise etwas mehr tun, z. B. die Definition der Iteratorkategorie, des Differenztyps, der Referenz usw., damit der Compiler den Iterator akzeptiert.]

Und die Ausgabe ist:

y
y
time1: 1833ns
time2: 2933ns

[Wenn Sie den Code ausführen, stimmen Ihre Ergebnisse natürlich nicht genau mit diesen überein.]

Selbst für diesen einfachen Fall (und nur etwa 80 Inkremente und Vergleiche) haben wir einer einfachen linearen Suche einen Overhead von etwa 60% hinzugefügt. Insbesondere wenn Iteratoren ursprünglich zu C ++ hinzugefügt wurden, hätten einige Leute ein Design mit so viel Aufwand einfach nicht akzeptiert. Sie wären wahrscheinlich nicht standardisiert worden, und selbst wenn sie es getan hätten, würde praktisch niemand sie verwenden.

Jerry Sarg
quelle
7

Der Unterschied besteht darin, was etwas ist und wie sich etwas verhält.

Viele Sprachen versuchen, die beiden miteinander zu verbinden, aber es sind ganz unterschiedliche Dinge.

Wenn wie ist was und was ist wie ...

Wenn alles davon erbt, ergeben sich objecteinige Vorteile wie: Jede Objektvariable kann jeden Wert enthalten. Aber das ist auch der Knackpunkt , alles muss sich verhalten ( die wie ) wie ein objectund sieht aus wie ( das , was ) ein object.

Aber:

  • Was ist, wenn Ihr Objekt keine aussagekräftige Definition von Gleichheit hat?
  • Was ist, wenn es keinen aussagekräftigen Hash hat?
  • Was ist, wenn Ihr Objekt nicht geklont werden kann, Objekte jedoch?

Entweder wird der objectTyp im Wesentlichen unbrauchbar - da das Objekt keine Gemeinsamkeit für alle möglichen Instanzen bietet. Oder es wird Objekte geben, die eine gebrochene / objectschuhhörnige / absurde Definition einer vermuteten universellen Eigenschaft haben, die bis auf eine Reihe von Fallstricken ein fast universelles Verhalten beweist.

Wenn Was nicht mit Wie verbunden ist

Alternativ können Sie das Was und das Wie getrennt halten. Dann können sich mehrere verschiedene Typen (mit überhaupt keinem gemeinsamen Was ) auf die gleiche Weise verhalten, wie es der Mitarbeiter beim Wie gesehen hat . In diesem Sinne ist die Idee eines Iteratornicht ein spezifisches Was , sondern ein Wie . Insbesondere wie interagieren Sie mit einer Sache, wenn Sie noch nicht wissen, mit was Sie interagieren ?

Java (und ähnliche) ermöglichen Ansätze dazu mithilfe von Schnittstellen. Eine Schnittstelle in dieser Hinsicht beschreibt die Kommunikationsmittel und implizit ein Protokoll der Kommunikation und des Handelns, das befolgt wird. Jedes Was, das sich als von einem bestimmten Wie deklariert , besagt, dass es die relevante Kommunikation und Aktion unterstützt, die im Protokoll beschrieben sind. Dadurch kann jeder Mitarbeiter auf dem verlassen Wie und ab steckenzubleiben nicht exakt angeben , welche Was ‚s verwendet werden kann.

C ++ (und ähnliche) ermöglichen Ansätze hierfür durch Ententypisierung. Einer Vorlage ist es egal, ob der kollaborierende Typ deklariert, dass er einem Verhalten folgt, nur dass innerhalb eines bestimmten Kompilierungskontexts das Objekt auf eine bestimmte Weise interagiert werden kann. Auf diese Weise können C ++ - Zeiger und Objekte, die bestimmte Operatoren überschreiben, von demselben Code verwendet werden. Weil sie die Checkliste erfüllen, um als gleichwertig zu gelten.

  • unterstützt * a, a->, ++ a und einen ++ -> Eingabe- / Weiterleitungsiterator
  • unterstützt * a, a->, ++ a, a ++, --a und a-- -> bidirektionalen Iterator

Der zugrunde liegende Typ muss nicht einmal einen Container iterieren, es kann auch was sein . Darüber hinaus können einige Mitarbeiter noch allgemeiner arbeiten. Stellen Sie sich vor, eine Funktion wird nur benötigt a++. Ein Iterator kann dies erfüllen, ebenso ein Zeiger, eine Ganzzahl und jedes implementierte Objekt operator++.

Unter- und Überspezifikation

Das Problem bei beiden Ansätzen liegt unter und über der Spezifikation.

Für die Verwendung einer Schnittstelle muss das Objekt deklarieren, dass es ein bestimmtes Verhalten unterstützt. Dies bedeutet auch, dass der Ersteller dies von Anfang an ausführen muss. Dies führt dazu, dass einige What 's den Schnitt nicht machen, da sie ihn nicht deklariert haben. Es bedeutet auch, dass immer Was einen gemeinsamen Vorfahren hat, die Schnittstelle, die das Wie darstellt . Dies kehrt zum ursprünglichen Problem von zurück object. Dies führt dazu, dass Mitarbeiter ihre Anforderungen zu stark spezifizieren, während gleichzeitig einige Objekte entweder aufgrund fehlender Deklaration unbrauchbar werden oder ausgeblendete Fallstricke auftreten, da ein erwartetes Verhalten schlecht definiert ist.

Die Verwendung einer Vorlage erfordert, dass der Mitarbeiter mit einem völlig unbekannten Was arbeitet und durch seine Interaktionen ein Wie definiert . In gewissem Maße erschwert dies das Schreiben von Mitarbeitern, da es das Was für seine Kommunikationsprimitive (Funktionen / Felder / usw.) analysieren und gleichzeitig Kompilierungsfehler vermeiden muss, oder zumindest darauf hinweisen muss, wie ein bestimmtes Was nicht seinen Anforderungen für das Wie entspricht . Dies ermöglicht es dem Mitarbeiter, das absolute Minimum von einem bestimmten Was zu fordern , wodurch der breiteste Bereich dessen verwendet werden kann , was verwendet werden soll. Leider hat dies den Nachteil, dass unsinnige Verwendungen von Objekten zugelassen werden, die technisch die Kommunikationsprimitive für eine bestimmte Person liefernWie , aber befolgen Sie nicht das implizite Protokoll, das das Auftreten aller möglichen schlechten Dinge zulässt.

Iteratoren

In diesem Fall ist ein Iteratorist ein Wie es eine Abkürzung für eine Beschreibung der Wechselwirkung ist. Alles, was dieser Beschreibung entspricht, ist per Definition ein Iterator. Wenn wir wissen, wie wir allgemeine Algorithmen schreiben können, haben wir eine kurze Liste mit ' Wie wird ein bestimmtes Was gegeben ? ', Das bereitgestellt werden muss, damit der Algorithmus funktioniert. Diese Liste enthält die Funktion / Eigenschaften / usw. Ihre Implementierung berücksichtigt die spezifischen Aspekte , mit denen sich der Algorithmus befasst.

Kain0_0
quelle
6

Da C ++ nicht brauchen zu haben (Auszug) Basisklassen Polymorphismus zu tun. Es hat sowohl strukturelle Subtypisierung als auch nominative Subtypisierung .

Verwirrenderweise im speziellen Fall von Iteratoren wurden frühere Standards std::iteratorals (ungefähr) definiert.

template <class Category, class T, class Distance = std::ptrdiff_t, class Pointer = T*, class Reference = T&>
struct iterator {
    using iterator_category = Category;
    using value_type = T;
    using difference_type = Distance;
    using pointer = Pointer;
    using reference = Reference;
}

Dh lediglich als Anbieter der erforderlichen Mitgliedstypen. Es hatte kein Laufzeitverhalten und war in C ++ 17 veraltet

Beachten Sie, dass auch dies keine gemeinsame Basis sein kann, da eine Klassenvorlage keine Klasse ist und jede Instanziierung für sich allein steht.

Caleth
quelle
5

Ein Grund ist, dass Iteratoren keine Instanzen einer Klasse sein müssen. Zeiger sind zum Beispiel in vielen Fällen vollkommen gute Iteratoren, und da dies Grundelemente sind, können sie nichts erben.

David Thornley
quelle