remove_if equivalente para std :: map

118

Eu estava tentando apagar uma série de elementos do mapa com base em uma condição particular. Como faço isso usando algoritmos STL?

Inicialmente pensei em usar, remove_ifmas não é possível, pois remove_if não funciona para container associativo.

Existe algum algoritmo equivalente "remove_if" que funciona para o mapa?

Como uma opção simples, pensei em percorrer o mapa e apagar. Mas repetir o mapa e apagar é uma opção segura? (Já que os iteradores ficam inválidos após o apagamento)

Usei o seguinte exemplo:

bool predicate(const std::pair<int,std::string>& x)
{
    return x.first > 2;
}

int main(void) 
{

    std::map<int, std::string> aMap;

    aMap[2] = "two";
    aMap[3] = "three";
    aMap[4] = "four";
    aMap[5] = "five";
    aMap[6] = "six";

//      does not work, an error
//  std::remove_if(aMap.begin(), aMap.end(), predicate);

    std::map<int, std::string>::iterator iter = aMap.begin();
    std::map<int, std::string>::iterator endIter = aMap.end();

    for(; iter != endIter; ++iter)
    {
            if(Some Condition)
            {
                            // is it safe ?
                aMap.erase(iter++);
            }
    }

    return 0;
}
aJ.
fonte
O que quer dizer que remove_if não funciona?
dirkgently de
Não posso usar remove_if para encontrar um elemento no mapa, certo? Ele deu um erro de tempo de compilação. Estou esquecendo de algo?
aJ.
Não - não funciona porque remove_if funciona reordenando uma sequência, movendo os elementos que falham na condição para o final. Portanto, ele funciona em um T [n], mas não em um mapa <T, U>.
MSalters em
2
Com C + 11, você pode usar for(auto iter=aMap.begin(); iter!=aMap.end(); ){ ....}para reduzir a desordem. O descanso é como os outros disseram. Esta pergunta me poupou um pouco de confusão agora ;-)
Atul Kumar

Respostas:

111

Quase.

for(; iter != endIter; ) {
     if (Some Condition) {
          iter = aMap.erase(iter);
     } else {
          ++iter;
     }
}

O que você tinha originalmente incrementaria o iterador duas vezes se você apagasse um elemento dele; você poderia potencialmente pular elementos que precisavam ser apagados.

Este é um algoritmo comum que vi usado e documentado em muitos lugares.

[EDIT] Você está correto ao dizer que os iteradores são invalidados após um apagamento, mas apenas os iteradores que fazem referência ao elemento que foi apagado, outros iteradores ainda são válidos. Portanto, usando iter++na erase()chamada.

Steve Folly
fonte
4
Estou confuso; por que você usaria para (; ...;) em vez de while (...)? Além disso, embora isso provavelmente funcione, .erase não retorna um iterador do próximo? Portanto, parece que o blog if (Some Condition) deveria ser iter = aMap.erase (iter) para ser o mais compatível. Talvez eu esteja faltando alguma coisa? Não tenho a experiência de alguns de vocês.
taxilian de
86
Observe que no C ++ 11 todos os contêineres associativos, incluindo map, retornam o próximo iterador de erase(iter). É muito mais limpo de fazer iter = erase( iter ).
Potatoswatter
10
@taxilian (anos de atraso) while () ou for () funcionaria, mas semanticamente, as pessoas costumam usar for () para iterar em um intervalo conhecido e while () para um número desconhecido de loops. Como o intervalo é conhecido neste caso (do início ao fimIter ), for () não seria uma escolha incomum e provavelmente seria mais comum. Mas, novamente, ambos seriam aceitáveis.
Jamin Gray de
4
@taxilian Mais importante: com 'for', você pode ter sua definição de iterador DENTRO do escopo do loop, para que não interfira com o resto do programa.
Sanchises
1
@athos A pergunta é formulada na voz passiva, "é recomendado." Não há recomendação universal. Acho que meu último comentário é a maneira mais direta. Envolve duas cópias da variável iteradora, que perde um pouco de eficiência, como alguém apontou aqui. É sua decisão o que é apropriado para você.
Potatoswatter
75

erase_if para std :: map (e outros contêineres)

Eu uso o seguinte modelo para isso.

namespace stuff {
  template< typename ContainerT, typename PredicateT >
  void erase_if( ContainerT& items, const PredicateT& predicate ) {
    for( auto it = items.begin(); it != items.end(); ) {
      if( predicate(*it) ) it = items.erase(it);
      else ++it;
    }
  }
}

Isso não retornará nada, mas removerá os itens do std :: map.

Exemplo de uso:

// 'container' could be a std::map
// 'item_type' is what you might store in your container
using stuff::erase_if;
erase_if(container, []( item_type& item ) {
  return /* insert appropriate test */;
});

Segundo exemplo (permite que você passe em um valor de teste):

// 'test_value' is value that you might inject into your predicate.
// 'property' is just used to provide a stand-in test
using stuff::erase_if;
int test_value = 4;  // or use whatever appropriate type and value
erase_if(container, [&test_value]( item_type& item ) {
  return item.property < test_value;  // or whatever appropriate test
});
Salvador de ferro
fonte
3
@CodeAngry Obrigado - sempre me pareceu estranho que isso ainda não existisse no std. Eu entendo por que não é membro de std::map, mas acho que algo parecido deveria estar na biblioteca padrão.
Iron Savior
3
Será adicionado em C ++ 20 parastd::map e outros.
Roi Danton de
3

Eu obtive esta documentação da excelente referência SGI STL :

Map tem a propriedade importante de que inserir um novo elemento em um mapa não invalida os iteradores que apontam para os elementos existentes. Apagar um elemento de um mapa também não invalida nenhum iterador, exceto, é claro, para iteradores que realmente apontam para o elemento que está sendo apagado.

Portanto, o iterador que você possui que está apontando para o elemento a ser apagado, obviamente, será invalidado. Faça algo assim:

if (some condition)
{
  iterator here=iter++;
  aMap.erase(here)
}
1800 INFORMAÇÕES
fonte
3
Isso não é diferente do código original. iter ++ incrementa o iterador e então retorna um iterador apontando para o elemento antes do incremento.
Steve Folly,
Mas iter não será invalidado, uma vez que apagamos na posição aqui
1800 INFORMAÇÕES
@ 1800INFORMAÇÕES: inserir uma chamada de função é um ponto de sequência, o efeito colateral do incremento é avaliado antes de eraseser chamado. Portanto, eles são de fato equivalentes. Ainda assim, eu prefiro fortemente sua versão em vez da original.
Peterchen
Funciona para matriz ou vetor, mas causará um resultado inesperado no mapa de stl.
hunter_tech
2

O código original tem apenas um problema:

for(; iter != endIter; ++iter)
{
    if(Some Condition)
    {
        // is it safe ?
        aMap.erase(iter++);
    }
}

Aqui, o iteré incrementado uma vez no loop for e outra vez no erase, o que provavelmente terminará em algum loop infinito.

partha biswas
fonte
2

Aqui está uma solução elegante.

for (auto it = map.begin(); it != map.end();)
{   
    (SomeCondition) ? map.erase(it++) : (++it);
}
Raiz de Mandragora
fonte
1

Das notas de fundo de:

http://www.sgi.com/tech/stl/PairAssociativeContainer.html

um Container Associativo de Par não pode fornecer iteradores mutáveis ​​(conforme definido nos requisitos do Trivial Iterator), porque o tipo de valor de um iterador mutável deve ser Atribuível e o par não é Atribuível. No entanto, um Container Associativo de Par pode fornecer iteradores que não são completamente constantes: iteradores de forma que a expressão (* i) .second = d seja válida.

piotr
fonte
1

Primeiro

Map tem a propriedade importante de que inserir um novo elemento em um mapa não invalida os iteradores que apontam para os elementos existentes. Apagar um elemento de um mapa também não invalida nenhum iterador, exceto, é claro, para iteradores que realmente apontam para o elemento que está sendo apagado.

Em segundo lugar, o código a seguir é bom

for(; iter != endIter; )
{
    if(Some Condition)
    {
        aMap.erase(iter++);
    }
    else
    {
        ++iter;
    }
}

Ao chamar uma função, os parâmetros são avaliados antes da chamada dessa função.

Portanto, quando o iter ++ é avaliado antes da chamada para apagar, o operador ++ do iterador retornará o item atual e apontará para o próximo item após a chamada.

Vincent
fonte
1

IMHO não há remove_if()equivalente.
Você não pode reordenar um mapa.
Portanto, remove_if()não pode colocar seus pares de interesse no final em que você pode ligar erase().

user109134
fonte
Isso é realmente lamentável.
allyourcode
1

Baseado na resposta do Iron Savior Para aqueles que gostariam de fornecer uma gama mais próxima das linhas de iteradores de tomadas funcionais padrão.

template< typename ContainerT, class FwdIt, class Pr >
void erase_if(ContainerT& items, FwdIt it, FwdIt Last, Pr Pred) {
    for (; it != Last; ) {
        if (Pred(*it)) it = items.erase(it);
        else ++it;
    }
}

Curioso para saber se há alguma maneira de perder os ContainerTitens e obtê-los do iterador.

Greg Domjan
fonte
1
"Os identificadores que começam com um sublinhado seguido por uma letra maiúscula são reservados para todo uso pela implementação."
YSC
0

A resposta de Steve Folly Eu me sinto mais eficiente.

Aqui está outra solução fácil, mas menos eficiente :

A solução usa remove_copy_ifpara copiar os valores que queremos em um novo contêiner e, em seguida, trocar o conteúdo do contêiner original pelo novo:

std::map<int, std::string> aMap;

...
//Temporary map to hold the unremoved elements
std::map<int, std::string> aTempMap;

//copy unremoved values from aMap to aTempMap
std::remove_copy_if(aMap.begin(), aMap.end(), 
                    inserter(aTempMap, aTempMap.end()),
                    predicate);

//Swap the contents of aMap and aTempMap
aMap.swap(aTempMap);
aJ.
fonte
2
Isso parece ineficiente.
allyourcode
0

Se você quiser apagar todos os elementos com chave maior que 2, a melhor maneira é

map.erase(map.upper_bound(2), map.end());

Porém, funciona apenas para intervalos, não para qualquer predicado.

Tadeusz Kopec
fonte
0

Eu uso assim

 std::map<int, std::string> users;    
 for(auto it = users.begin(); it <= users.end()) {
    if(<condition>){
      it = users.erase(it);
    } else {
    ++it;
    }
 }
voltento
fonte