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 - 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].
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.
++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 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.
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 0nella 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 endfosse 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::listo 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à.