Por que os intervalos do iterador padrão [começam, terminam) em vez de [começam, terminam]?

204

Por que o Padrão define end()como um passado após o final, em vez de no final real?

Cachorro
fonte
19
Eu estou supondo "porque é isso que o padrão diz" não vai funcionar, certo? :)
Luchian Grigore
39
@LuchianGrigore: Claro que não. Isso acabaria com o nosso respeito pelo (povo por trás) do padrão. Devemos esperar que haja uma razão para as escolhas feitas pelo padrão.
Kerrek SB
4
Em resumo, os computadores não contam como pessoas. Mas se você está curioso para saber por que as pessoas não contam como computadores, recomendo O Nada Que É: Uma História Natural de Zero para uma análise aprofundada do problema que os seres humanos descobriram que existe um número menos um do que um.
John McFarlane
8
Como existe apenas uma maneira de gerar a "última", geralmente não é barata, porque precisa ser real. Gerar "você caiu do fim do penhasco" é sempre barato, muitas representações possíveis servirão. (vazio *) "ahhhhhhh" vai dar certo.
21711
6
Olhei a data da pergunta e, por um segundo, pensei que você estivesse brincando.
Asaf

Respostas:

286

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 - begintempos. 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 1em todos os lugares nos algoritmos baseados em intervalo é uma conseqüência direta e a motivação da convenção [início, fim].

Kerrek SB
fonte
2
O loop C típico para iteração sobre uma matriz de tamanho N é "para (i = 0; i <N; i ++) a [i] = 0;". Agora, você não pode expressar isso diretamente com os iteradores - muitas pessoas perderam tempo tentando fazer <significativo. Mas é quase igualmente óbvio dizer "para (i = 0; i! = N; i ++) ..." Mapear 0 para começar e N para terminar é, portanto, conveniente.
Krazy Glew
3
@KrazyGlew: Eu não coloquei tipos no meu exemplo de loop deliberadamente. Se você pensa em begine endcomo ints com valores 0e 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.
Kerrek SB
4
@KerrekSB: Concordo que "nunca descobrimos que [! = É melhor] até que começamos a pensar em coleções mais gerais". O IMHO é uma das coisas pelas quais Stepanov merece crédito - falando como alguém que tentou escrever essas bibliotecas de modelos antes do STL. No entanto, discutirei sobre "! =" Ser mais natural - ou melhor, argumentarei que! = Provavelmente introduziu bugs, que <seriam detectados. Pense for (i = 0; i = 100;! I + = 3) ...
Krazy Glew
@KrazyGlew: Seu último ponto é algo fora de tópico, pois a sequência {0, 3, 6, ..., 99} não é da forma que o OP perguntou. Se você deseja que seja assim, escreva um ++modelo de iterador incrementável step_by<3>, que terá a semântica anunciada originalmente.
Kerrek SB
@KrazyGlew Mesmo que <em algum momento oculte um bug, é um bug de qualquer maneira . Se alguém usa !=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.
Phil1970
80

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 beginaponta para o início da sequência e endaponta para o final da mesma sequência. A desreferenciação beginacessa o elemento A, e a desreferenciação endnão faz sentido porque não há nenhum elemento certo para ele. Além disso, adicionar um iterador ino meio fornece

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

e você vê imediatamente que o intervalo de elementos de begina icontém os elementos Ae Benquanto o intervalo de elementos de ia endcontém os elementos Ce D. A desreferenciação ifornece 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 Be C. No entanto, devido à reversão da sequência, agora o elemento Bestá à direita.

celtschk
fonte
2
Esta é IMHO a melhor resposta, embora eu ache que seria melhor ilustrar se os iteradores apontassem para números e os elementos estivessem entre os números (a sintaxe 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 ".
Supercat
@ supercat: Os números não deveriam indicar posições / índices do iterador, mas sim os próprios elementos. Vou substituir os números por letras para deixar isso mais claro. De fato, com os números fornecidos, begin[0](assumindo um iterador de acesso aleatório) acessaria o elemento 1, pois não há elemento 0na minha sequência de exemplo.
celtschk
Por que a palavra "begin" é usada em vez de "start"? Afinal, "begin" é um verbo.
user1741137
@ user1741137 Acho que "begin" deve ser a abreviação de "begin" (que agora faz sentido). "começo" é muito longo, "começo" soa como um bom ajuste. "start" estaria em conflito com o verbo "start" (por exemplo, quando você tiver que definir uma função start()em sua classe para iniciar um processo específico ou qualquer outra coisa, seria irritante se conflitar com um já existente).
Fareanor 24/06
74

Por que o Padrão define end()como um passado após o final, em vez de no final real?

Porque:

  1. Evita manuseio especial para intervalos vazios. Para intervalos vazios, begin()é igual a end()&
  2. Isso simplifica o critério final para loops que iteram sobre os elementos: os loops simplesmente continuam enquanto end()não forem atingidos.
Alok Save
fonte
64

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 .


Além disso:

Se o endfoi antes do último elemento, como você estaria insert()no final verdadeiro ?!

user541686
fonte
2
Esta é uma resposta altamente subestimada. Os exemplos são concisos e diretos ao ponto, e os "Também" não foram ditos por mais ninguém e são o tipo de coisas que parecem muito óbvias em retrospecto, mas me parecem revelações.
underscore_d
@underscore_d: Obrigado !! :)
user541686
Aliás, caso eu pareça um hipócrita por não ter votado, é porque eu já estava lá em julho de 2016!
underscore_d
@underscore_d: hahaha eu nem percebi, mas obrigado! :)
user541686
22

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.

Ken Bloom
fonte
8

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++;
}
Anders Abel
fonte
0
  1. Se um contêiner estiver vazio begin() == end(),.
  2. Os programadores de C ++ tendem a usar em !=vez de <(menos que) em condições de loop, portanto, end()é conveniente apontar para uma posição inicial.
Andreas DM
fonte