Iteratore C ++, perché non esiste una classe base Iteratore da cui tutti gli iteratori ereditano


11

Sto imparando per un esame e ho una domanda per la quale faccio fatica a rispondere e rispondere.

Perché non esiste una classe base iteratore da cui ereditano tutti gli altri iteratori?

Suppongo che il mio insegnante si riferisca alla struttura gerarchica dal riferimento cpp " http://prntscr.com/mgj542 " e dobbiamo fornire un motivo diverso da quello per cui dovrebbero?

So cosa sono gli iteratori (una sorta di) e che sono abituati a lavorare su contenitori. Da quello che ho capito a causa delle diverse possibili strutture di dati sottostanti, contenitori diversi hanno iteratori diversi perché è possibile accedere in modo casuale a un array, ad esempio, ma non un elenco collegato e contenitori diversi richiedono modi diversi di spostarsi attraverso di essi.

Probabilmente sono modelli specializzati a seconda del contenitore, giusto?


2
Condividere la tua ricerca aiuta tutti . Raccontaci cosa hai provato e perché non ha soddisfatto le tue esigenze. Ciò dimostra che hai impiegato del tempo per cercare di aiutarti, ci salva dal ribadire risposte ovvie e soprattutto ti aiuta a ottenere una risposta più specifica e pertinente. Vedi anche Come chiedere
moscerino

5
" Perché non esiste una classe base di iteratori da cui tutti gli altri iteratori ereditano? " Uhm ... perché dovrebbe essercene uno?
Nicol Bolas,

Risposte:


14

Hai già ottenuto risposte indicando perché non è necessario che tutti gli iteratori ereditino da una singola classe di base Iterator. Sarei andato un po 'oltre però. Uno degli obiettivi del C ++ è l'astrazione con zero costi di runtime.

Se gli iteratori funzionassero tutti ereditando da una classe base comune e utilizzassero le funzioni virtuali nella classe base per definire l'interfaccia e le classi derivate fornissero implementazioni di tali funzioni virtuali, ciò potrebbe (e spesso vorrebbe) aggiungere un tempo di esecuzione sostanziale spese generali per le operazioni coinvolte.

Consideriamo, ad esempio, una semplice gerarchia di iteratori che utilizza l'ereditarietà e le funzioni virtuali:

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

Diamo quindi un breve test:

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

[nota: a seconda del compilatore, potrebbe essere necessario fare un po 'di più, ad esempio definire iterator_category, differenza_tipo, riferimento e così via, affinché il compilatore accetti l'iteratore.]

E l'output è:

y
y
time1: 1833ns
time2: 2933ns

[Naturalmente, se si esegue il codice, i risultati non corrisponderanno esattamente a questi.]

Quindi, anche per questo semplice caso (e facendo solo circa 80 incrementi e confronti) abbiamo aggiunto circa il 60% di spese generali a una semplice ricerca lineare. Soprattutto quando gli iteratori sono stati originariamente aggiunti al C ++, un bel po 'di persone semplicemente non avrebbe accettato un progetto con così tanto sovraccarico. Probabilmente non sarebbero stati standardizzati e, anche se lo fossero, praticamente nessuno li avrebbe usati.


7

La differenza è tra cos'è qualcosa e come si comporta qualcosa.

Molte lingue cercano di confondere le due cose insieme, ma sono cose abbastanza distinte.

Se come è cosa e cosa è come ...

Se tutto eredita da objectallora, si verificano alcuni vantaggi come: qualsiasi variabile di oggetto può contenere qualsiasi valore in assoluto. Ma questo è anche il problema, tutto deve comportarsi ( il come ) come un object, e sembrare ( il cosa ) un object.

Ma:

  • Cosa succede se il tuo oggetto non ha una definizione significativa di uguaglianza?
  • E se non avesse un hash significativo?
  • Cosa succede se il tuo oggetto non può essere clonato, ma gli oggetti possono essere?

O il objecttipo diventa essenzialmente inutile, a causa dell'oggetto che non fornisce alcuna comunanza tra tutte le possibili istanze. Oppure esisteranno oggetti che hanno una definizione spezzata / con il clacson / assurda di una presunta proprietà universale trovata sulla objectquale si rivela un comportamento quasi universale tranne che per un certo numero di gotchas.

Se cosa non è legato a come

In alternativa puoi tenere separati cosa e come . Quindi diversi tipi (con nulla in comune per niente che cosa ) possono tutti comportarsi allo stesso modo visto dal collaboratore come . In questo senso l'idea di un Iteratornon è un cosa specifico , ma un come . In particolare Come interagisci con una cosa quando non sai ancora con cosa stai interagendo.

Java (e simili) consentono approcci a questo utilizzando interfacce. Un'interfaccia a questo proposito descrive i mezzi di comunicazione e implicitamente un protocollo di comunicazione e azione che viene seguito. Qualsiasi cosa che si dichiari di essere di un dato How , afferma che supporta la comunicazione e l'azione pertinenti delineate dal protocollo. Ciò consente a qualsiasi collaboratore di fare affidamento sul How e di non impantanarsi specificando esattamente che cosa è possibile utilizzare.

C ++ (e simili) consentono approcci a questo tramite la tipizzazione duck. A un modello non importa se il tipo collaboratore dichiara di seguire un comportamento, solo che in un determinato contesto di compilazione, l'oggetto può essere interagito in un modo particolare. Ciò consente ai puntatori C ++ e agli oggetti che sovrastano operatori specifici di essere utilizzati dallo stesso codice. Perché soddisfano la lista di controllo per essere considerati equivalenti.

  • supporta * a, a->, ++ a e a ++ -> input / forward iterator
  • supporta * a, a->, ++ a, a ++, --a e a-- -> iteratore bidirezionale

Il tipo sottostante non deve nemmeno iterare un contenitore, potrebbe essere qualsiasi cosa . Inoltre, consente ad alcuni collaboratori di essere ancora più generici, immaginare che una funzione ha solo bisogno a++, un iteratore può soddisfarlo, così può un puntatore, così può un numero intero, così l'implementazione di qualsiasi oggetto operator++.

Sotto e sopra le specifiche

Il problema con entrambi gli approcci è sotto e sopra le specifiche.

L'uso di un'interfaccia richiede che l'oggetto dichiari di supportare un determinato comportamento, il che significa anche che il creatore deve ispirarlo fin dall'inizio. Questo fa sì che alcuni Che cosa non effettuino il taglio, in quanto non lo hanno dichiarato. Significa anche che qualunque cosa abbia un antenato comune, l'interfaccia che rappresenta il How . Questo fa risalire al problema iniziale di object. Questo fa sì che i collaboratori specifichino eccessivamente i loro requisiti, causando al contempo alcuni oggetti inutilizzabili a causa della mancanza di una dichiarazione o nascondendo i gotcha poiché un comportamento previsto è scarsamente definito.

L'uso di un modello richiede che il collaboratore lavori con un Cosa completamente sconosciuto e, attraverso le sue interazioni, definisce un Come . In una certa misura ciò rende più difficile la scrittura di collaboratori, in quanto deve analizzare il What for per le sue primitive di comunicazione (funzioni / campi / ecc.) Evitando errori di compilazione, o almeno sottolineare come un dato What non corrisponde ai suoi requisiti per il How . Ciò consente al collaboratore di richiedere il minimo assoluto da qualsiasi dato Cosa , consentendo la più ampia gamma di ciò che deve essere utilizzato. Sfortunatamente questo ha il rovescio della medaglia nel consentire usi insensati di oggetti che tecnici forniscono le primitive comunicative per un datoCome , ma non seguire il protocollo implicito che consente il verificarsi di ogni sorta di cose cattive.

iteratori

In questo caso un Iteratorè un modo in cui è una scorciatoia per una descrizione dell'interazione. Tutto ciò che corrisponde a quella descrizione è per definizione un Iterator. Sapere come ci permette di scrivere algoritmi generali e avere un breve elenco di " Come viene dato un cosa specifico " che deve essere fornito per far funzionare l'algoritmo. Tale elenco è la funzione / proprietà / ecc., La loro implementazione tiene conto dello specifico Cosa viene gestito dall'algoritmo.


6

Perché il C ++ non deve avere classi di base (astratte) per eseguire il polimorfismo. Ha subtyping strutturale nonché subtyping nominativo .

Confusamente nel caso particolare di Iteratori, gli standard precedenti definiti std::iteratorcome (approssimativamente)

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

Vale a dire semplicemente come fornitore dei tipi di membri richiesti. Non ha avuto alcun comportamento di runtime ed è stato deprecato in C ++ 17

Si noti che anche questa non può essere una base comune, poiché un modello di classe non è una classe, ogni istanza è indipendente dalle altre.



5

Uno dei motivi è che gli iteratori non devono essere istanze di una classe. I puntatori sono iteratori perfettamente buoni in molti casi, per esempio, e poiché quelli sono primitivi non possono ereditare da nulla.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.