¿Por qué define el estándar? end()
como uno pasado el final, en lugar de en el final real?
¿Por qué define el estándar? end()
como uno pasado el final, en lugar de en el final real?
Respuestas:
El mejor argumento fácilmente es el que hizo el propio Dijkstra :
Desea que el tamaño del rango sea un simple final de diferencia : comience ;
incluir el límite inferior es más "natural" cuando las secuencias degeneran en vacías, y también porque la alternativa ( excluyendo el límite inferior) requeriría la existencia de un valor centinela "uno antes del comienzo".
Todavía necesita justificar por qué comienza a contar en cero en lugar de uno, pero eso no era parte de su pregunta.
La sabiduría detrás de la convención [comenzar, finalizar] vale la pena una y otra vez cuando tiene algún tipo de algoritmo que trata con múltiples llamadas anidadas o iteradas a construcciones basadas en rangos, que se encadenan naturalmente. Por el contrario, el uso de un rango doblemente cerrado incurriría en códigos extraños y extremadamente desagradables y ruidosos. Por ejemplo, considere una partición [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Otro ejemplo es el ciclo de iteración estándar for (it = begin; it != end; ++it)
, que se ejecutaend - begin
tiempos. El código correspondiente sería mucho menos legible si ambos extremos fueran inclusivos, e imagine cómo manejaría los rangos vacíos.
Finalmente, también podemos hacer un buen argumento de por qué el conteo debe comenzar en cero: con la convención medio abierta para rangos que acabamos de establecer, si se le da un rango de N elementos (por ejemplo, enumerar los miembros de una matriz), entonces 0 es el "comienzo" natural para que pueda escribir el rango como [0, N ), sin correcciones ni compensaciones incómodas.
En pocas palabras: el hecho de que no veamos el número 1
en todas partes en los algoritmos basados en rango es una consecuencia directa de la convención [comienzo, fin] y motivación.
begin
y end
como int
s con valores 0
y N
, respectivamente, encaja perfectamente. Podría decirse que es la !=
condición más natural que la tradicional <
, pero nunca lo descubrimos hasta que empezamos a pensar en colecciones más generales.
++
plantilla de iterador incremental step_by<3>
, que luego tendría la semántica anunciada originalmente.
!=
cuando debería usar <
, entonces es un error. Por cierto, ese rey del error es fácil de encontrar con pruebas unitarias o afirmaciones.
En realidad, muchas cosas relacionadas con iteradores de repente tienen mucho más sentido si consideras que los iteradores no apuntan a los elementos de la secuencia, sino en el medio , con la eliminación de la referencia para acceder al siguiente elemento directamente. Entonces el iterador de "un extremo pasado" de repente tiene sentido inmediato:
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
begin end
Obviamente begin
apunta al comienzo de la secuencia y end
apunta al final de la misma secuencia. La desreferenciación begin
accede al elemento A
, y la desreferenciación end
no tiene sentido porque no tiene ningún elemento correcto. Además, agregar un iterador i
en el medio da
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
e inmediatamente ve que el rango de elementos de begin
a i
contiene los elementos A
y B
mientras que el rango de elementos de i
a end
contiene los elementos C
y D
. Desreferenciari
le da al elemento el derecho, es el primer elemento de la segunda secuencia.
Incluso el "off-by-one" para iteradores inversos de repente se vuelve obvio de esa manera: invertir esa secuencia da:
+---+---+---+---+
| D | C | B | A |
+---+---+---+---+
^ ^ ^
| | |
rbegin ri rend
(end) (i) (begin)
He escrito los correspondientes iteradores no inversos (base) entre paréntesis a continuación. Verá, el iterador inverso que pertenece a i
(que he nombrado ri
) todavía apunta entre elementos B
y C
. Sin embargo, debido a la inversión de la secuencia, ahora el elemento B
está a la derecha.
foo[i]
) es una abreviatura del elemento inmediatamente después de la posición i
). Pensando en ello, me pregunto si podría ser útil para un lenguaje tener operadores separados para "elemento inmediatamente después de la posición i" y "elemento inmediatamente antes de la posición i", ya que muchos algoritmos funcionan con pares de elementos adyacentes y dicen " Los elementos a cada lado de la posición i "pueden estar más limpios que" Los elementos en las posiciones i e i + 1 ".
begin[0]
(suponiendo un iterador de acceso aleatorio) accedería al elemento 1
, ya que no hay ningún elemento 0
en mi secuencia de ejemplo.
start()
en su clase para iniciar un proceso específico o lo que sea, sería molesto si entra en conflicto con una ya existente).
¿Por qué el Estándar defineend()
como uno pasado el final, en lugar de en el final real?
Porque:
begin()
es igual a
end()
& end()
no se alcanza.Porque entonces
size() == end() - begin() // For iterators for whom subtraction is valid
y no tendrás que hacer cosas incómodas como
// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }
y no escribirás accidentalmente código erróneo como
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
Además: ¿Qué find()
devolvería si end()
apuntara a un elemento válido?
¿ Realmente quieres que se llame a otro miembro invalid()
que devuelva un iterador no válido?
Dos iteradores ya son lo suficientemente dolorosos ...
Ah, y mira esta publicación relacionada .
Si el end
fue antes del último elemento, ¿cómo llegarías insert()
al verdadero final?
El idioma de iterador de rangos semicerrados [begin(), end())
se basa originalmente en la aritmética de puntero para matrices simples. En ese modo de operación, tendría funciones a las que se les pasó una matriz y un tamaño.
void func(int* array, size_t size)
La conversión a rangos semicerrados [begin, end)
es muy simple cuando tiene esa información:
int* begin;
int* end = array + size;
for (int* it = begin; it < end; ++it) { ... }
Para trabajar con rangos completamente cerrados, es más difícil:
int* begin;
int* end = array + size - 1;
for (int* it = begin; it <= end; ++it) { ... }
Dado que los punteros a las matrices son iteradores en C ++ (y la sintaxis fue diseñada para permitir esto), es mucho más fácil llamar std::find(array, array + size, some_value)
que llamar std::find(array, array + size - 1, some_value)
.
Además, si trabaja con rangos semicerrados, puede usar el !=
operador para verificar la condición final, porque (si sus operadores están definidos correctamente) <
implica !=
.
for (int* it = begin; it != end; ++ it) { ... }
Sin embargo, no hay una manera fácil de hacer esto con rangos completamente cerrados. Estás atrapado con <=
.
El único tipo de iterador que admite <
y >
opera en C ++ son los iteradores de acceso aleatorio. Si tuviera que escribir un <=
operador para cada clase de iterador en C ++, tendría que hacer que todos sus iteradores fueran totalmente comparables, y tendría menos opciones para crear iteradores menos capaces (como los iteradores bidireccionales activados std::list
o los iteradores de entrada) que funcionan iostreams
) si C ++ usa rangos completamente cerrados.
Con el end()
señalador más allá del final, es fácil iterar una colección con un bucle for:
for (iterator it = collection.begin(); it != collection.end(); it++)
{
DoStuff(*it);
}
Al end()
señalar el último elemento, un bucle sería más complejo:
iterator it = collection.begin();
while (!collection.empty())
{
DoStuff(*it);
if (it == collection.end())
break;
it++;
}
begin() == end()
.!=
lugar de <
(menos de) en condiciones de bucle, por lo tanto, end()
es conveniente apuntar a una posición fuera del extremo.