it-swarm.com.de

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

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?

11
jnt

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.

14
Jerry Coffin

Der Unterschied besteht zwischen Was etwas ist und Wie etwas verhält sich.

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

Wenn wie ist was und was ist wie ...

Wenn alles von object erbt, ergeben sich einige Vorteile wie: Jede Objektvariable kann einen beliebigen Wert enthalten. Aber das ist auch das Problem, alles muss sich verhalten ( das Wie) wie ein object und aussehen 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 Typ object im Wesentlichen unbrauchbar - da das Objekt keine Gemeinsamkeit für alle möglichen Instanzen bietet. Oder es wird Objekte geben, die eine gebrochene/schuhhörnige/absurde Definition einer vermuteten universellen Eigenschaft haben, die auf object gefunden wurde und die fast universelles Verhalten mit Ausnahme einer Reihe von Fallstricken beweist.

Wenn Was nicht mit Wie verbunden ist

Alternativ können Sie Was und Wie getrennt halten. Dann können sich mehrere verschiedene Typen (mit überhaupt nichts gemeinsam das Was) alle so verhalten, wie es vom Mitarbeiter gesehen wird das Wie. In diesem Sinne ist die Idee eines Iterator kein spezifisches was, sondern ein wie. Speziell Wie Interagierst du mit einer Sache, wenn du noch nicht weißt Was mit der du interagierst.

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 gegeben deklariert Wie, gibt an, dass es die relevante Kommunikation und Aktion unterstützt, die im Protokoll beschrieben sind. Dies ermöglicht es jedem Mitarbeiter, sich auf das Wie zu verlassen und sich nicht zu verzetteln, indem genau angegeben wird, welches Was 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, er kann ein beliebiger was sein. Darüber hinaus können einige Mitarbeiter noch allgemeiner arbeiten. Stellen Sie sich vor, eine Funktion benötigt nur a++ kann ein Iterator dies erfüllen, ebenso ein Zeiger, eine Ganzzahl und jedes Objekt, das operator++.

nter- 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 Was den Schnitt nicht ausführen, da sie ihn nicht deklariert haben. Es bedeutet auch, dass immer Was einen gemeinsamen Vorfahren hat, wobei die Schnittstelle das Wie darstellt. Dies kehrt zum ursprünglichen Problem von object zurück. Dies führt dazu, dass Mitarbeiter ihre Anforderungen zu stark spezifizieren und gleichzeitig einige Objekte entweder aufgrund fehlender Deklaration unbrauchbar werden oder versteckte Fallstricke sind, 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 auf seine Kommunikationsprimitive (Funktionen/Felder/usw.) analysieren muss, während Kompilierungsfehler vermieden werden, oder zumindest darauf hinweisen muss, wie ein bestimmtes Was entspricht nicht den Anforderungen für How. Dies ermöglicht es dem Mitarbeiter, das absolute Minimum von jedem gegebenen Was zu verlangen, wodurch der breiteste Bereich von was verwendet werden kann. Leider hat dies den Nachteil, dass unsinnige Verwendungen von Objekten zugelassen werden, die technisch die Kommunikationsprimitive für ein bestimmtes How liefern, aber nicht dem implizierten Protokoll folgen, das das Auftreten aller möglichen schlechten Dinge zulässt.

Iteratoren

In diesem Fall ist ein Iterator ein How Es ist eine Abkürzung für eine Beschreibung der Interaktion. Alles, was dieser Beschreibung entspricht, ist per Definition ein Iterator. Wenn wir Wie kennen, können wir allgemeine Algorithmen schreiben und eine kurze Liste von ' Wie haben, denen ein spezifisches Was' gegeben wurde. das muss bereitgestellt werden, damit der Algorithmus funktioniert. Diese Liste enthält die Funktion/Eigenschaften/usw. Ihre Implementierung berücksichtigt die spezifischen Was, die vom Algorithmus behandelt werden.

7
Kain0_0

Weil C++ keine (abstrakten) Basisklassen benötigt, um Polymorphismus zu betreiben. Es hat strukturelle Subtypisierung sowie nominative Subtypisierung .

Verwirrenderweise haben im speziellen Fall von Iteratoren frühere Standards std::iterator as (ungefähr)

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

Das heißt, als bloßer 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.

6
Caleth

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

5
David Thornley