Por que o Padrão define end()
como um passado após o final, em vez de no final real?
Por que o Padrão define end()
como um passado após o final, em vez de no final real?
Respostas:
O melhor argumento é facilmente o do próprio Dijkstra :
Você quer que o tamanho do intervalo para ser um simples diferença final - começar ;
incluir o limite inferior é mais "natural" quando as seqüências degeneram para vazias e também porque a alternativa ( excluindo o limite inferior) exigiria a existência de um valor sentinela "um antes do início".
Você ainda precisa justificar por que começa a contar com zero, em vez de um, mas isso não fazia parte da sua pergunta.
A sabedoria por trás da convenção [começo, fim] compensa várias vezes quando você tem algum tipo de algoritmo que lida com várias chamadas aninhadas ou iteradas para construções baseadas em intervalo, que são encadeadas naturalmente. Por outro lado, o uso de um intervalo duplamente fechado resultaria em códigos isolados e extremamente desagradáveis e barulhentos. Por exemplo, considere uma partição [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Outro exemplo é o loop de iteração padrão for (it = begin; it != end; ++it)
, que executa os end - begin
tempos. O código correspondente seria muito menos legível se as duas extremidades fossem inclusivas - e imagine como você lidaria com intervalos vazios.
Finalmente, também podemos argumentar bem por que a contagem deve começar em zero: com a convenção semi-aberta para os intervalos que acabamos de estabelecer, se você receber um intervalo de N elementos (digamos, para enumerar os membros de uma matriz), então 0 é o "começo" natural, para que você possa escrever o intervalo como [0, N ), sem deslocamentos ou correções difíceis.
Em poucas palavras: o fato de não vermos o número 1
em todos os lugares nos algoritmos baseados em intervalo é uma conseqüência direta e a motivação da convenção [início, fim].
begin
e end
como int
s com valores 0
e N
, respectivamente, ele se encaixa perfeitamente. Indiscutivelmente, é a !=
condição mais natural que a tradicional <
, mas nunca descobrimos isso até começarmos a pensar em coleções mais gerais.
++
modelo de iterador incrementável step_by<3>
, que terá a semântica anunciada originalmente.
!=
quando deve usar <
, então é um bug. A propósito, esse rei do erro é fácil de encontrar com testes ou afirmações de unidade.
Na verdade, muitas coisas relacionadas ao iterador de repente fazem muito mais sentido se você considerar que os iteradores não apontam para os elementos da sequência, mas no meio , com a desreferenciação acessando o próximo elemento à sua direita. Então, o iterador "one end end" faz repentinamente sentido imediato:
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
begin end
Obviamente begin
aponta para o início da sequência e end
aponta para o final da mesma sequência. A desreferenciação begin
acessa o elemento A
, e a desreferenciação end
não faz sentido porque não há nenhum elemento certo para ele. Além disso, adicionar um iterador i
no meio fornece
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
e você vê imediatamente que o intervalo de elementos de begin
a i
contém os elementos A
e B
enquanto o intervalo de elementos de i
a end
contém os elementos C
e D
. A desreferenciação i
fornece o elemento certo, que é o primeiro elemento da segunda sequência.
Mesmo o "off-by-one" para iteradores reversos repentinamente se torna óbvio assim: a reversão dessa sequência fornece:
+---+---+---+---+
| D | C | B | A |
+---+---+---+---+
^ ^ ^
| | |
rbegin ri rend
(end) (i) (begin)
Escrevi os iteradores não reversos (base) correspondentes entre parênteses abaixo. Você vê, o iterador reverso pertencente a i
( ao qual eu nomeei ri
) ainda aponta entre os elementos B
e C
. No entanto, devido à reversão da sequência, agora o elemento B
está à direita.
foo[i]
) é uma abreviação para o item imediatamente após a posição i
). Pensando nisso, gostaria de saber se seria útil para uma linguagem ter operadores separados para "item imediatamente após a posição i" e "item imediatamente antes da posição i", pois muitos algoritmos trabalham com pares de itens adjacentes e dizem " Os itens em ambos os lados da posição i "podem ser mais limpos que" Os itens nas posições iei + 1 ".
begin[0]
(assumindo um iterador de acesso aleatório) acessaria o elemento 1
, pois não há elemento 0
na minha sequência de exemplo.
start()
em sua classe para iniciar um processo específico ou qualquer outra coisa, seria irritante se conflitar com um já existente).
Por que o Padrão define end()
como um passado após o final, em vez de no final real?
Porque:
begin()
é igual a
end()
& end()
não forem atingidos.Porque então
size() == end() - begin() // For iterators for whom subtraction is valid
e você não terá que fazer coisas estranhas como
// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }
e você acidentalmente não escreverá códigos errados 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
Além disso: o que find()
retornaria se end()
apontado para um elemento válido?
Você realmente quer outro membro do chamado invalid()
que retorna um iterador inválido ?!
Dois iteradores já são dolorosos o suficiente ...
Ah, e veja este post relacionado .
Se o end
foi antes do último elemento, como você estaria insert()
no final verdadeiro ?!
O idioma do iterador de intervalos semi-fechados [begin(), end())
é originalmente baseado na aritmética do ponteiro para matrizes simples. Nesse modo de operação, você teria funções que receberam uma matriz e um tamanho.
void func(int* array, size_t size)
A conversão para intervalos semi-fechados [begin, end)
é muito simples quando você tem essa informação:
int* begin;
int* end = array + size;
for (int* it = begin; it < end; ++it) { ... }
Para trabalhar com faixas totalmente fechadas, é mais difícil:
int* begin;
int* end = array + size - 1;
for (int* it = begin; it <= end; ++it) { ... }
Como os ponteiros para matrizes são iteradores em C ++ (e a sintaxe foi projetada para permitir isso), é muito mais fácil chamar std::find(array, array + size, some_value)
do que chamar std::find(array, array + size - 1, some_value)
.
Além disso, se você trabalha com faixas semi-fechadas, pode usar o !=
operador para verificar a condição final, porque (se seus operadores estão definidos corretamente) <
implica !=
.
for (int* it = begin; it != end; ++ it) { ... }
No entanto, não há uma maneira fácil de fazer isso com faixas totalmente fechadas. Você está preso <=
.
O único tipo de iterador que suporta <
e >
opera em C ++ são iteradores de acesso aleatório. Se você tivesse que escrever um <=
operador para cada classe de iterador em C ++, seria necessário tornar todos os seus iteradores totalmente comparáveis e haveria menos opções para criar iteradores menos capazes (como os iteradores bidirecionais emstd::list
ou os iteradores de entrada operam iostreams
) se o C ++ usasse faixas totalmente fechadas.
Com o end()
apontador no final, é fácil iterar uma coleção com um loop for:
for (iterator it = collection.begin(); it != collection.end(); it++)
{
DoStuff(*it);
}
Ao end()
apontar para o último elemento, um loop seria mais complexo:
iterator it = collection.begin();
while (!collection.empty())
{
DoStuff(*it);
if (it == collection.end())
break;
it++;
}
begin() == end()
,.!=
vez de <
(menos que) em condições de loop, portanto, end()
é conveniente apontar para uma posição inicial.