Perché lo standard definisce end()
uno oltre la fine, anziché alla fine effettiva?
Perché lo standard definisce end()
uno oltre la fine, anziché alla fine effettiva?
Risposte:
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 - begin
tempi. 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 1
ovunque negli algoritmi basati sul range è una diretta conseguenza e motivazione della convenzione [inizio, fine].
begin
e end
come a int
s con valori 0
e 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.
++
modello iteratore -incrementabile step_by<3>
, che avrebbe quindi la semantica pubblicizzata originariamente.
!=
quando dovrebbe usare <
, allora è un bug. A proposito, quel re dell'errore è facile da trovare con test unitari o asserzioni.
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 begin
punta all'inizio della sequenza e end
punta alla fine della stessa sequenza. Il dereferenziamento begin
accede all'elemento A
e il dereferenziamento end
non ha senso perché non esiste alcun elemento giusto per esso. Inoltre, l'aggiunta di un iteratore i
nel mezzo dà
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
e vedrai immediatamente che l'intervallo di elementi da begin
a i
contiene gli elementi A
e B
mentre l'intervallo di elementi da i
a end
contiene gli elementi C
e D
. La dereferenziazione i
fornisce 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 B
e C
. Tuttavia, a causa dell'inversione della sequenza, ora l'elemento B
è sulla destra.
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 ".
begin[0]
(supponendo un iteratore ad accesso casuale) accederesti all'elemento 1
, dato che non c'è nessun elemento 0
nella mia sequenza di esempio.
start()
nella tua classe per avviare un processo specifico o altro, sarebbe fastidioso se fosse in conflitto con uno già esistente).
Perché lo standard definisce end()
uno oltre la fine, anziché alla fine effettiva?
Perché:
begin()
è uguale a
end()
& end()
non vengono raggiunti.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 .
Se end
fosse prima dell'ultimo elemento, come saresti insert()
alla fine vera ?!
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::list
o gli iteratori di input che funzionano su iostreams
) se C ++ utilizzava intervalli completamente chiusi.
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++;
}
begin() == end()
.!=
invece di <
(meno di) in condizioni di loop, quindi end()
è conveniente puntare a una posizione fuori dall'estremità.