Perché le gamme di iteratori standard [inizio, fine) anziché [inizio, fine]?


204

Perché lo standard definisce end()uno oltre la fine, anziché alla fine effettiva?


19
Sto indovinando "perché è quello che dice lo standard" non lo taglierà, giusto? :)
Luchian Grigore

39
@LuchianGrigore: Certo che no. Ciò eroderebbe il nostro rispetto per (le persone dietro) lo standard. Dovremmo aspettarci che ci sia una ragione per le scelte fatte dallo standard.
Kerrek SB

4
In breve, i computer non contano come le persone. Ma se sei curioso di sapere perché le persone non contano come i computer, ti consiglio Il nulla che è: una storia naturale di zero per uno sguardo approfondito ai problemi che gli umani hanno scoperto scoprendo che c'è un numero che è uno in meno di uno.
John McFarlane,

8
Poiché esiste un solo modo per generare "l'ultimo", spesso non è economico perché deve essere reale. Generare "sei caduto dalla fine della scogliera" è sempre economico, lo faranno molte possibili rappresentazioni. (vuoto *) "ahhhhhhh" andrà bene.
Hans Passant

6
Ho guardato la data della domanda e per un secondo ho pensato che stavi scherzando.
Asaf

Risposte:


286

La migliore argomentazione è facilmente quella fatta dallo stesso Dijkstra :

  • Volete che la dimensione dell'intervallo sia una semplice differenza fine  -  inizio ;

  • includere il limite inferiore è più "naturale" quando le sequenze degenerano in vuote, e anche perché l'alternativa ( escluso il limite inferiore) richiederebbe l'esistenza di un valore sentinella "uno prima dell'inizio".

Devi ancora giustificare il motivo per cui inizi a contare da zero anziché da uno, ma questo non faceva parte della tua domanda.

La saggezza alla base della convenzione [inizio, fine] ripaga di volta in volta quando si ha qualsiasi tipo di algoritmo che si occupa di più chiamate nidificate o ripetute a costruzioni basate su intervallo, che si incatenano naturalmente. Al contrario, l'uso di un intervallo doppiamente chiuso comporterebbe codici off-by-one ed estremamente spiacevoli e rumorosi. Ad esempio, considera una partizione [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Un altro esempio è il ciclo di iterazione standard for (it = begin; it != end; ++it), che esegue i end - begintempi. Il codice corrispondente sarebbe molto meno leggibile se entrambe le estremità fossero inclusive - e immagina come gestiresti intervalli vuoti.

Infine, possiamo anche fare una bella argomentazione sul perché il conteggio dovrebbe iniziare da zero: con la convenzione semi-aperta per gli intervalli che abbiamo appena stabilito, se ti viene dato un intervallo di N elementi (diciamo per enumerare i membri di un array), allora 0 è il "principio" naturale in modo da poter scrivere l'intervallo come [0, N ), senza alcun offset o correzione imbarazzante.

In breve: il fatto che non vediamo il numero 1ovunque negli algoritmi basati sul range è una diretta conseguenza e motivazione della convenzione [inizio, fine].


2
La tipica C per il ciclo che scorre su un array di dimensioni N è "for (i = 0; i <N; i ++) a [i] = 0;". Ora, non puoi esprimerlo direttamente con gli iteratori: molte persone hanno perso tempo cercando di rendere <significativo. Ma è quasi altrettanto ovvio dire "per (i = 0; i! = N; i ++) ..." Mappare 0 per iniziare e N per finire è quindi conveniente.
Krazy Glew,

3
@KrazyGlew: non ho inserito deliberatamente i tipi nel mio esempio di loop. Se pensi a begine endcome a ints con valori 0e N, rispettivamente, si adatta perfettamente. Probabilmente, è la !=condizione più naturale del tradizionale <, ma non l'abbiamo mai scoperto fino a quando non abbiamo iniziato a pensare a collezioni più generali.
Kerrek SB,

4
@KerrekSB: concordo sul fatto che "non abbiamo mai scoperto che [! = È meglio] fino a quando non abbiamo iniziato a pensare a collezioni più generali". IMHO è una delle cose per cui Stepanov merita il merito - parlando come qualcuno che ha provato a scrivere tali librerie di modelli prima della STL. Comunque, discuterò sul fatto che "! =" Sia più naturale - o, piuttosto, sosterrò che! = Probabilmente ha introdotto dei bug, che <catturerebbe. Pensa per (i = 0; i! = 100; i + = 3) ...
Krazy Glew

@KrazyGlew: il tuo ultimo punto è in qualche modo fuori tema, poiché la sequenza {0, 3, 6, ..., 99} non è della forma richiesta dall'OP. Se vuoi che sia così, dovresti scrivere un ++modello iteratore -incrementabile step_by<3>, che avrebbe quindi la semantica pubblicizzata originariamente.
Kerrek SB,

@KrazyGlew Anche se <a volte nascondesse un bug, è comunque un bug . Se qualcuno usa !=quando dovrebbe usare <, allora è un bug. A proposito, quel re dell'errore è facile da trovare con test unitari o asserzioni.
Fil1970,

80

In realtà, un sacco di roba iteratore correlata rende improvvisamente molto più senso se si considera l'iteratori non puntando agli elementi della sequenza, ma in mezzo , con dereferenziazione accesso al successivo elemento diritto. Quindi l'iteratore "one past end" improvvisamente ha immediatamente senso:

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^               ^
   |               |
 begin            end

Ovviamente beginpunta all'inizio della sequenza e endpunta alla fine della stessa sequenza. Il dereferenziamento beginaccede all'elemento Ae il dereferenziamento endnon ha senso perché non esiste alcun elemento giusto per esso. Inoltre, l'aggiunta di un iteratore inel mezzo dà

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
 begin     i      end

e vedrai immediatamente che l'intervallo di elementi da begina icontiene gli elementi Ae Bmentre l'intervallo di elementi da ia endcontiene gli elementi Ce D. La dereferenziazione ifornisce l'elemento giusto, ovvero il primo elemento della seconda sequenza.

Perfino lo "off-by-one" per gli iteratori inversi diventa improvvisamente ovvio in questo modo: Invertire questa sequenza dà:

   +---+---+---+---+
   | D | C | B | A |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
rbegin     ri     rend
 (end)    (i)   (begin)

Ho scritto i corrispondenti iteratori non inversi (di base) tra parentesi qui sotto. Vedete, l'iteratore inverso appartenente a i(che ho chiamato ri) punta ancora tra elementi Be C. Tuttavia, a causa dell'inversione della sequenza, ora l'elemento Bè sulla destra.


2
Questa è la risposta migliore di IMHO, anche se penso che potrebbe essere meglio illustrato se gli iteratori indicassero i numeri e gli elementi tra i numeri (la sintassi foo[i]) è una scorciatoia per l'elemento immediatamente dopo la posizione i). Pensandoci, mi chiedo se potrebbe essere utile che una lingua abbia operatori separati per "oggetto immediatamente dopo la posizione i" e "oggetto immediatamente prima della posizione i", poiché molti algoritmi funzionano con coppie di oggetti adiacenti e dicendo " Gli oggetti su entrambi i lati della posizione i "possono essere più puliti di" Gli oggetti nelle posizioni i e i + 1 ".
supercat,

@supercat: I numeri non dovevano indicare posizioni / indici dell'iteratore, ma indicare gli elementi stessi. Sostituirò i numeri con le lettere per renderlo più chiaro. In effetti, con i numeri forniti, begin[0](supponendo un iteratore ad accesso casuale) accederesti all'elemento 1, dato che non c'è nessun elemento 0nella mia sequenza di esempio.
Celtschk,

Perché viene utilizzata la parola "inizio" anziché "inizio"? Dopotutto, "inizio" è un verbo.
user1741137

@ user1741137 Penso che "inizio" significhi essere l'abbreviazione di "inizio" (che ora ha senso). "inizio" è troppo lungo, "inizio" sembra una bella scelta. "start" sarebbe in conflitto con il verbo "start" (ad esempio quando devi definire una funzione start()nella tua classe per avviare un processo specifico o altro, sarebbe fastidioso se fosse in conflitto con uno già esistente).
Fareanor

74

Perché lo standard definisce end()uno oltre la fine, anziché alla fine effettiva?

Perché:

  1. Evita la gestione speciale per intervalli vuoti. Per intervalli vuoti, begin()è uguale a end()&
  2. Rende semplice il criterio di fine per i loop che ripetono gli elementi: i loop continuano semplicemente fino a quando end()non vengono raggiunti.

64

Perché poi

size() == end() - begin()   // For iterators for whom subtraction is valid

e non dovrai fare imbarazzante cose come

// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }

e non scriverai accidentalmente codice errato come

bool empty() { return begin() == end() - 1; }    // a typo from the first version
                                                 // of this post
                                                 // (see, it really is confusing)

bool empty() { return end() - begin() == -1; }   // Signed/unsigned mismatch
// Plus the fact that subtracting is also invalid for many iterators

Inoltre: cosa find()restituirebbe se end()indicato a un elemento valido?
Ti davvero vuole un'altra membro del Tribunale chiamato invalid()che restituisce un iteratore non valida ?!
Due iteratori sono già abbastanza dolorosi ...

Oh, e guarda questo post correlato .


Anche:

Se endfosse prima dell'ultimo elemento, come saresti insert()alla fine vera ?!


2
Questa è una risposta altamente sottovalutata. Gli esempi sono concisi e diretti al punto, e gli "Anche" non sono stati detti da nessun altro e sono il tipo di cose che sembrano molto ovvie in retrospettiva ma mi colpiscono come rivelazioni.
underscore_d

@underscore_d: grazie !! :)
user541686

a proposito, nel caso in cui sembri un ipocrita per non aver votato, è perché l'ho già fatto a luglio 2016!
underscore_d

@underscore_d: ahahah non me ne sono nemmeno accorto, ma grazie! :)
user541686

22

Il linguaggio iteratore di intervalli semichiusi [begin(), end())si basa in origine sull'aritmetica del puntatore per array semplici. In quella modalità operativa, avresti funzioni a cui sono state passate una matrice e una dimensione.

void func(int* array, size_t size)

La conversione in intervalli semichiusi [begin, end)è molto semplice quando si dispone di tali informazioni:

int* begin;
int* end = array + size;

for (int* it = begin; it < end; ++it) { ... }

Per lavorare con intervalli completamente chiusi, è più difficile:

int* begin;
int* end = array + size - 1;

for (int* it = begin; it <= end; ++it) { ... }

Poiché i puntatori alle matrici sono iteratori in C ++ (e la sintassi è stata progettata per consentire ciò), è molto più facile chiamare std::find(array, array + size, some_value)che chiamarestd::find(array, array + size - 1, some_value) .


Inoltre, se si lavora con intervalli semichiusi, è possibile utilizzare l' !=operatore per verificare le condizioni finali, poiché (se gli operatori sono definiti correttamente) <implica !=.

for (int* it = begin; it != end; ++ it) { ... }

Tuttavia, non esiste un modo semplice per farlo con intervalli completamente chiusi. Sei bloccato con<= .

L'unico tipo di iteratore che supporta <e le >operazioni in C ++ sono iteratori ad accesso casuale. Se dovessi scrivere un <=operatore per ogni classe di iteratori in C ++, dovresti rendere tutti i tuoi iteratori completamente comparabili e meno scelte per la creazione di iteratori meno capaci (come gli iteratori bidirezionali std::listo gli iteratori di input che funzionano su iostreams) se C ++ utilizzava intervalli completamente chiusi.


8

Con il end()puntamento oltre la fine, è facile iterare una raccolta con un ciclo for:

for (iterator it = collection.begin(); it != collection.end(); it++)
{
    DoStuff(*it);
}

Con l' end()indicazione dell'ultimo elemento, un ciclo sarebbe più complesso:

iterator it = collection.begin();
while (!collection.empty())
{
    DoStuff(*it);

    if (it == collection.end())
        break;

    it++;
}

0
  1. Se un contenitore è vuoto, begin() == end().
  2. I programmatori C ++ tendono a usare !=invece di <(meno di) in condizioni di loop, quindi end()è conveniente puntare a una posizione fuori dall'estremità.
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.