vector :: at vs. vector :: operator []

95

Eu sei que at()é mais lento do que []por causa de sua verificação de limite, que também é discutida em questões semelhantes como C ++ Vector at / [] operator speed ou :: std :: vector :: at () vs operator [] << resultados surpreendentes !! 5 a 10 vezes mais lento / rápido! . Só não entendo para que at()serve o método.

Se eu tenho um vetor simples como este: std::vector<int> v(10);e decido acessar seus elementos usando em at()vez de []em situação quando eu tenho um índice ie não tenho certeza se ele está nos limites de vetores, isso me força a envolvê-lo com try-catch bloco :

try
{
    v.at(i) = 2;
}
catch (std::out_of_range& oor)
{
    ...
}

embora eu seja capaz de obter o mesmo comportamento usando size()e verificando o índice por conta própria, o que parece mais fácil e muito conveniente para mim:

if (i < v.size())
    v[i] = 2;

Portanto, minha pergunta é:
Quais são as vantagens de usar vector :: at em vez de vector :: operator [] ?
Quando devo usar vector :: at em vez de vector :: size + vector :: operator [] ?

LihO
fonte
11
1 pergunta muito boa !! mas eu não acho que () é aquele comumente usado.
Rohit Vipin Mathews de
10
Observe que em seu código de exemplo if (i < v.size()) v[i] = 2;, há um caminho de código possível que não é atribuído 2a nenhum elemento de v. Se esse for o comportamento correto, ótimo. Mas muitas vezes não há nada sensato que essa função possa fazer quando i >= v.size(). Portanto, não há nenhuma razão específica para não usar uma exceção para indicar uma situação inesperada. Muitas funções apenas usam operator[]sem uma verificação em relação ao tamanho, documento que ideve estar dentro do intervalo e culpam o UB resultante no chamador.
Steve Jessop

Respostas:

74

Eu diria que as exceções que vector::at()lançam não têm a intenção de serem capturadas pelo código imediatamente circundante. Eles são úteis principalmente para detectar bugs em seu código. Se você precisar verificar os limites no tempo de execução porque, por exemplo, o índice vem da entrada do usuário, é melhor você fazer uma ifinstrução. Portanto, em resumo, projete seu código com a intenção de vector::at()nunca lançar uma exceção, de forma que, se isso acontecer, e seu programa abortar, seja um sinal de um bug. (como um assert())

pmdj
fonte
1
+1 Eu gosto da explicação de como separar o manuseio da entrada do usuário errado (validação de entrada; entrada inválida pode ser esperada, então não é considerada algo excepcional) ... e bugs no código (desreferenciar iterador que está fora do intervalo é excepcional coisa)
Bojan Komazec
Então você diz que eu devo usar size()+ []quando o índice depende da entrada do usuário, use assertem situações onde o índice nunca deve estar fora dos limites para fácil correção de bugs no futuro e .at()em todas as outras situações (apenas no caso, porque algo errado pode acontecer .. .)
LihO
8
@LihO: se sua implementação oferece uma implementação de depuração de, vectorentão provavelmente é melhor usar isso como a opção "just in case" em vez de em at()qualquer lugar. Dessa forma, você pode esperar um pouco mais de desempenho no modo de liberação, caso precise.
Steve Jessop
3
Sim, a maioria das implementações STL hoje em dia suporta um modo de depuração que verifica até mesmo operator[], por exemplo, gcc.gnu.org/onlinedocs/libstdc++/manual/… então, se sua plataforma suportar isso, é melhor você ir com ele!
pmdj
1
@pmdj ponto fantástico, que eu não sabia ... mas link órfão. : P atual é: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
underscore_d
16

me força a embrulhá-lo com o bloco try-catch

Não, não (o bloco try / catch pode ser upstream). É útil quando você deseja que uma exceção seja lançada em vez de seu programa entrar no domínio de comportamento indefinido.

Concordo que a maioria dos acessos fora dos limites a vetores é um erro do programador (nesse caso, você deve usar assertpara localizar esses erros mais facilmente; a maioria das versões de depuração de bibliotecas padrão fazem isso automaticamente para você). Você não quer usar exceções que podem ser engolidas no upstream para relatar erros do programador: você quer ser capaz de consertar o bug .

Como é improvável que um acesso fora dos limites a um vetor faça parte do fluxo normal do programa (caso seja, você está certo: verifique antes com em sizevez de deixar a exceção borbulhar), concordo com seu diagnóstico: até essencialmente inútil.

Alexandre C.
fonte
Se eu não detectar a out_of_rangeexceção, abort()será chamado.
LihO de
@LihO: Não necessariamente ... eles try..catchpodem estar presentes no método que está chamando este método.
Naveen
12
No mínimo, até útil na medida em que, de outra forma, você se pegaria escrevendo algo assim if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }. As pessoas costumam pensar em funções de lançamento de exceções em termos de "maldições, eu tenho que lidar com a exceção", mas contanto que você documente cuidadosamente o que cada uma de suas funções pode lançar, elas também podem ser usadas como "ótimo, não tem que verificar uma condição e lançar uma exceção ".
Steve Jessop
@SteveJessop: Não gosto de lançar exceções para bugs de programa, pois eles podem ser detectados pelo upstream por outros programadores. As afirmações são muito mais úteis aqui.
Alexandre C.
6
@AlexandreC. bem, a resposta oficial para isso é que out_of_rangederiva de logic_error, e outros programadores "deveriam" saber melhor do que pegar logic_errors upstream e ignorá-los. asserttambém pode ser ignorado se seus colegas não quiserem saber sobre seus erros, é apenas mais difícil porque eles precisam compilar seu código com NDEBUG;-) Cada mecanismo tem seus méritos e falhas.
Steve Jessop
11

Quais são as vantagens de usar vector :: at sobre vector :: operator []? Quando devo usar vector :: at em vez de vector :: size + vector :: operator []?

O ponto importante aqui é que as exceções permitem a separação do fluxo normal de código da lógica de tratamento de erros, e um único bloco catch pode tratar problemas gerados a partir de uma miríade de sites de lançamento, mesmo se espalhados profundamente nas chamadas de função. Portanto, não at()é necessariamente mais fácil para um único uso, mas às vezes se torna mais fácil - e menos ofuscante para a lógica do caso normal - quando você tem muita indexação para validar.

Também é digno de nota que, em alguns tipos de código, um índice está sendo incrementado de maneiras complexas e usado continuamente para pesquisar um array. Nesses casos, é muito mais fácil garantir verificações corretas usando at().

Como exemplo do mundo real, tenho um código que transforma o C ++ em elementos lexicais e outro código que move um índice sobre o vetor de tokens. Dependendo do que for encontrado, posso desejar incrementar e verificar o próximo elemento, como em:

if (token.at(i) == Token::Keyword_Enum)
{
    ASSERT_EQ(tokens.at(++i), Token::Idn);
    if (tokens.at(++i) == Left_Brace)
        ...
    or whatever

Nesse tipo de situação, é muito difícil verificar se você chegou ao final da entrada de maneira inadequada, pois isso depende muito dos tokens exatos encontrados. A verificação explícita em cada ponto de uso é dolorosa e há muito mais espaço para erros do programador à medida que incrementos pré / pós, compensações no ponto de uso, raciocínio falho sobre a validade contínua de algum teste anterior etc.

Tony Delroy
fonte
10

at pode ser mais claro se você tiver um ponteiro para o vetor:

return pVector->at(n);
return (*pVector)[n];
return pVector->operator[](n);

Desempenho à parte, o primeiro deles é o código mais simples e claro.

Brangdon
fonte
... especialmente quando você precisa de um ponteiro para o n elemento -ésimo de um vector.
golfinho de
4

Primeiro, não é especificado se at()ou operator[]é mais lento. Quando não há erro de limite, eu esperaria que eles tivessem a mesma velocidade, pelo menos em compilações de depuração. A diferença é que at()especifica exatamente o que vai acontecer se houver um erro de limite (uma exceção), onde, como no caso de operator[], é um comportamento indefinido - uma falha em todos os sistemas que uso (g ++ e VC ++), pelo menos quando os sinalizadores de depuração normais são usados. (Outra diferença é que, depois de ter certeza do meu código, posso obter um aumento substancial de velocidade operator[] desativando a depuração. Se o desempenho exigir - eu não faria isso a menos que fosse necessário.)

Na prática, at()raramente é apropriado. Se o contexto é tal que você sabe que o índice pode ser inválido, você provavelmente quer o teste explícito (por exemplo, para retornar um valor padrão ou algo assim), e se você sabe que não pode ser inválido, você deseja abortar (e se você não sabe se pode ser inválido ou não, sugiro que especifique a interface de sua função com mais precisão). Existem algumas exceções, no entanto, em que o índice inválido pode resultar da análise de dados do usuário e o erro deve causar o cancelamento de toda a solicitação (mas não interromper o funcionamento do servidor); nesses casos, uma exceção é apropriada e at()fará isso por você.

James Kanze
fonte
4
Por que você esperaria que eles tivessem a mesma velocidade, quando operator[]não é forçado a verificar os limites, mas sim at()? Isso implica problemas de cache, especulação e buffer de ramificação?
Sebastian Mach
@phresnel operator[]não é necessário para fazer a verificação de limites, mas todas as boas implementações o fazem. Pelo menos no modo de depuração. A única diferença é o que eles fazem se o índice estiver fora dos limites: operator[]aborta com uma mensagem de erro, at()lança uma exceção.
James Kanze
2
Desculpe, perdi seu atributo "em modo de depuração". No entanto, eu não mediria a qualidade do código no modo de depuração. No modo de liberação, a verificação só é exigida por at().
Sebastian Mach
1
@phresnel A maior parte do código que entreguei está no modo "depurar". Você só desativa a verificação quando os problemas de desempenho realmente exigem. (Microsoft pré-2010 era um pouco problemático aqui, pois std::stringnem sempre funcionava se as opções de verificação não correspondessem às do tempo de execução: -MDe é melhor você desativar a verificação -MDd, e é melhor você ter ligado.)
James Kanze
2
Eu sou mais do campo que diz "código sancionado (garantido) pelo padrão"; é claro que você está livre para entregar no modo de depuração, mas ao fazer desenvolvimento de plataforma cruzada (incluindo, mas não exclusivamente, o caso do mesmo sistema operacional, mas diferentes versões do compilador), confiar no padrão é a melhor aposta para lançamentos e modo de depuração é considerada uma ferramenta para o programador conseguir que a coisa seja mais correta e robusta :)
Sebastian Mach
1

O objetivo de usar exceções é que o código de tratamento de erros pode estar mais distante.

Nesse caso específico, a entrada do usuário é de fato um bom exemplo. Imagine que você deseja analisar semanticamente uma estrutura de dados XML que usa índices para se referir a algum tipo de recurso armazenado internamente em a std::vector. Agora a árvore XML é uma árvore, então você provavelmente deseja usar recursão para analisá-la. No fundo, na recursão, pode haver uma violação de acesso por parte do gravador do arquivo XML. Nesse caso, você geralmente deseja eliminar todos os níveis de recursão e apenas rejeitar o arquivo inteiro (ou qualquer tipo de estrutura "mais grosseira"). É aqui que o at vem a calhar. Você pode simplesmente escrever o código de análise como se o arquivo fosse válido. O código da biblioteca cuidará da detecção de erros e você pode apenas capturar o erro no nível grosseiro.

Além disso, outros contêineres, como std::map, também têm o std::map::atque tem uma semântica ligeiramente diferente do que std::map::operator[]: at pode ser usado em um mapa const, enquanto operator[]não pode. Agora, se você quiser escrever um código agnóstico de contêiner, como algo que pode lidar com const std::vector<T>&ou const std::map<std::size_t, T>&, ContainerType::atseria sua arma preferida.

No entanto, todos esses casos geralmente aparecem ao lidar com algum tipo de entrada de dados não validada. Se você tiver certeza sobre seu intervalo válido, como normalmente deveria estar, geralmente pode usar operator[], mas melhor ainda, iteradores com begin()e end().

ltjax
fonte
1

De acordo com este artigo, desempenho à parte, não faz diferença usar atou operator[], apenas se o acesso for garantido estar dentro do tamanho do vetor. Caso contrário, se o acesso for baseado apenas na capacidade do vetor, é mais seguro usar at.

ahj
fonte
1
lá fora existem dragões. o que acontece se clicarmos nesse link? (dica: eu já sei, mas no StackOverflow nós preferimos comentários que não sofram danos no link, ou seja, forneça um breve resumo sobre o que você quer dizer)
Sebastian Mach
Obrigado pela dica. Está consertado agora.
ahj
0

Observação: parece que algumas pessoas novas estão rejeitando essa resposta sem ter a cortesia de dizer o que está errado. A resposta abaixo está correta e pode ser verificada aqui .

Na verdade, há apenas uma diferença: ata verificação de limites, enquanto operator[]não. Isso se aplica a compilações de depuração, bem como a compilações de lançamento, e isso é muito bem especificado pelos padrões. É simples assim.

Isso torna o atmétodo mais lento, mas também é um conselho muito ruim para não usar at. Você tem que olhar para números absolutos, não para números relativos. Posso apostar com segurança que a maior parte do seu código está fazendo operações muito mais caras do que at. Pessoalmente, tento usar atporque não quero que um bug desagradável crie um comportamento indefinido e entre furtivamente na produção.

Shital Shah
fonte
1
As exceções em C ++ pretendem ser um mecanismo de tratamento de erros, não uma ferramenta de depuração. Herb Sutter explica por que jogar std::out_of_rangeou qualquer forma de arremesso std::logic_erroré, de fato, um erro lógico por si só aqui .
Big Temp
@BigTemp - Não tenho certeza de como seu comentário se relaciona a esta pergunta e resposta. Sim, exceções são um tópico altamente debatido, mas a questão aqui é a diferença entre ate []e minha resposta simplesmente afirma a diferença. Eu pessoalmente uso o método "seguro" quando o desempenho não é um problema. Como diz Knuth, não faça uma otimização prematura. Além disso, é bom detectar bugs mais cedo do que na produção, independentemente das diferenças filosóficas.
Shital Shah