Preciso passar por um conjunto e remover elementos que atendam a um critério predefinido.
Este é o código de teste que escrevi:
#include <set>
#include <algorithm>
void printElement(int value) {
std::cout << value << " ";
}
int main() {
int initNum[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
std::set<int> numbers(initNum, initNum + 10);
// print '0 1 2 3 4 5 6 7 8 9'
std::for_each(numbers.begin(), numbers.end(), printElement);
std::set<int>::iterator it = numbers.begin();
// iterate through the set and erase all even numbers
for (; it != numbers.end(); ++it) {
int n = *it;
if (n % 2 == 0) {
// wouldn't invalidate the iterator?
numbers.erase(it);
}
}
// print '1 3 5 7 9'
std::for_each(numbers.begin(), numbers.end(), printElement);
return 0;
}
Inicialmente, pensei que apagar um elemento do conjunto ao iterá-lo invalidaria o iterador, e o incremento no loop for teria um comportamento indefinido. Mesmo assim, eu executei esse código de teste e tudo correu bem, e não sei explicar o porquê.
Minha pergunta: esse é o comportamento definido para conjuntos std ou essa implementação é específica? Estou usando o gcc 4.3.3 no ubuntu 10.04 (versão de 32 bits), a propósito.
Obrigado!
Solução proposta:
Essa é uma maneira correta de iterar e apagar elementos do conjunto?
while(it != numbers.end()) {
int n = *it;
if (n % 2 == 0) {
// post-increment operator returns a copy, then increment
numbers.erase(it++);
} else {
// pre-increment operator increments, then return
++it;
}
}
Edit: SOLUÇÃO PREFERIDA
Encontrei uma solução que me parece mais elegante, embora faça exatamente o mesmo.
while(it != numbers.end()) {
// copy the current iterator then increment it
std::set<int>::iterator current = it++;
int n = *current;
if (n % 2 == 0) {
// don't invalidate iterator it, because it is already
// pointing to the next element
numbers.erase(current);
}
}
Se houver várias condições de teste durante esse período, cada uma delas deverá incrementar o iterador. Gosto mais desse código porque o iterador é incrementado apenas em um lugar , tornando o código menos propenso a erros e mais legível.
++it
deve ser um pouco mais eficiente do queit++
porque não requer o uso de uma cópia temporária invisível do iterador. A versão da Kornel, embora mais longa, garante que os elementos não filtrados sejam iterados com mais eficiência.Respostas:
Isso depende da implementação:
Norma 23.1.2.8:
Talvez você possa tentar isso - isso é padrão:
Observe que ++ é postfix, portanto, passa a posição antiga para apagar, mas primeiro salta para uma mais nova devido ao operador.
Atualização 10/10/2015: C ++ 11 resolveu o defeito.
iterator erase (const_iterator position);
retorne um iterador para o elemento que segue o último elemento removido (ouset::end
, se o último elemento foi removido). Portanto, o estilo C ++ 11 é:fonte
deque
no MSVC2013. A implementação deles é com erros ou ainda há outro requisito que impede que isso funcionedeque
. A especificação STL é tão complicada que você não pode esperar que todas as implementações a sigam, muito menos seu programador casual memorizá-la. O STL é um monstro além de domesticar, e como não há uma implementação exclusiva (e os conjuntos de testes, se houver) aparentemente não cobrem casos tão óbvios como a exclusão de elementos em um loop), que fazem do STL um brinquedo quebradiço brilhante que pode ser usado com um estrondo quando você olha de lado.Se você executar seu programa através do valgrind, verá vários erros de leitura. Em outras palavras, sim, os iteradores estão sendo invalidados, mas você está tendo sorte no seu exemplo (ou muito azar, pois não está vendo os efeitos negativos do comportamento indefinido). Uma solução para isso é criar um iterador temporário, aumentar a temperatura, excluir o iterador de destino e definir o destino para a temperatura. Por exemplo, reescreva seu loop da seguinte maneira:
fonte
while
loop. ou seja,for ( ; it != numbers.end(); )
é melhor visível comwhile (it != numbers.end())
Você entende mal o que significa "comportamento indefinido". Comportamento indefinido não significa "se você fizer isso, seu programa irá falhar ou produzir resultados inesperados." Significa "se você fizer isso, seu programa poderá travar ou produzir resultados inesperados" ou fazer qualquer outra coisa, dependendo do compilador, do sistema operacional, da fase da lua, etc.
Se algo for executado sem travar e se comportar como você espera, isso é não prova que não é um comportamento indefinido. Tudo o que prova é que seu comportamento foi o observado para essa execução específica após a compilação com esse compilador específico naquele sistema operacional específico.
A exclusão de um elemento de um conjunto invalida o iterador para o elemento apagado. O uso de um iterador invalidado é um comportamento indefinido. Aconteceu que o comportamento observado era o que você pretendia nesse caso em particular; isso não significa que o código está correto.
fonte
if (n > 2 && n < 7 )
, obtenho 0 1 2 4 7 8 9. - O resultado específico aqui provavelmente depende mais dos detalhes de implementação do método erase e dos iteradores definidos, em vez da fase da lua (não aquela deve sempre confiar nos detalhes da implementação). ;)std::set::erase
retornar um iterador, para que seu código MSVC funcione rapidamente quando compilado pelo gcc" ou "A Microsoft faz verificações vinculadasstd::bitset::operator[]
para que seu algoritmo de conjunto de bits cuidadosamente otimizado diminua para um rastrear quando compilado com o MSVC ". STL não tem nenhuma implementação único e sua especificação é uma bagunça inchado crescimento exponencial, por isso não admira exclusão de elementos de dentro de um loop exige conhecimentos sênior programador ...Apenas para avisar que, no caso de um contêiner deque, todas as soluções que verificam a igualdade do iterador de deque para numbers.end () provavelmente falharão no gcc 4.8.4. Ou seja, apagar um elemento do deque geralmente invalida o ponteiro para numbers.end ():
Resultado:
Observe que, embora a transformação de deque esteja correta nesse caso específico, o ponteiro final foi invalidado ao longo do caminho. Com o deque de um tamanho diferente, o erro é mais aparente:
Resultado:
Aqui está uma das maneiras de corrigir isso:
fonte
do not trust an old remembered dq.end() value, always compare to a new call to dq.end()
.O C ++ 20 terá "apagamento uniforme de contêiner" e você poderá escrever:
E que irá trabalhar para
vector
,set
,deque
, etc. Veja cppReference para mais informações.fonte
Esse comportamento é específico da implementação. Para garantir a correção do iterador, você deve usar "it = numbers.erase (it);" instrução se você precisar excluir o elemento e simplesmente iterador de incertezas em outro caso.
fonte
Set<T>::erase
versão não retorna o iterador.gcc 4.8
comc++1y
tem um erro em apagar.it = collection.erase(it);
é suposto para trabalhar, mas pode ser mais seguro usarcollection.erase(it++);
Eu acho que o uso do método STL '
remove_if
' from poderia ajudar a evitar algum problema estranho ao tentar excluir o objeto envolvido pelo iterador.Esta solução pode ser menos eficiente.
Digamos que temos algum tipo de contêiner, como vetor ou uma lista chamada m_bullets:
'
it
' é o iterador que 'remove_if
' retorna, o terceiro argumento é uma função lambda que é executada em todos os elementos do contêiner. Como o contêiner contémBullet::Ptr
, a função lambda precisa obter esse tipo (ou uma referência a esse tipo) passado como argumento.'
remove_if
' remove o contêiner em que a função lambda retornou true e muda esse conteúdo para o início do contêiner. O 'it
' aponta para um objeto indefinido que pode ser considerado lixo. Objetos de 'it' a m_bullets.end () podem ser apagados, pois ocupam memória, mas contêm lixo, portanto, o método 'erase' é chamado nesse intervalo.fonte
Me deparei com o mesmo problema antigo e encontrei abaixo o código mais compreensível, que é de certa forma as soluções acima.
fonte