remove_if équivalent pour std :: map


118

J'essayais d'effacer une gamme d'éléments de la carte en fonction de conditions particulières. Comment le faire en utilisant les algorithmes STL?

Au départ j'ai pensé à utiliser remove_ifmais ce n'est pas possible car remove_if ne fonctionne pas pour les conteneurs associatifs.

Existe-t-il un algorithme équivalent "remove_if" qui fonctionne pour la carte?

En guise d'option simple, j'ai pensé à parcourir la carte et à effacer. Mais est-ce que faire une boucle sur la carte et effacer une option sûre? (Car les itérateurs deviennent invalides après l'effacement)

J'ai utilisé l'exemple suivant:

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;
}

Que voulez-vous dire par le fait que remove_if ne fonctionne pas?
dirkgently

Je ne peux pas utiliser remove_if pour trouver un élément dans la carte, non? Il a donné une erreur de compilation. Est-ce que je manque quelque chose?
aJ.

Non - cela ne fonctionne pas car remove_if fonctionne en réorganisant une séquence, en déplaçant les éléments qui échouent à la condition vers la fin. Par conséquent, cela fonctionne sur un T [n], mais pas sur une application <T, U>.
MSalters le

2
Avec C + 11, vous pouvez utiliser for(auto iter=aMap.begin(); iter!=aMap.end(); ){ ....}pour réduire l'encombrement. Le repos est comme d'autres l'ont dit. Cette question m'a évité de me fendre les cheveux pour le moment ;-)
Atul Kumar

Réponses:


111

Presque.

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

Ce que vous aviez à l'origine incrémenterait l'itérateur deux fois si vous en supprimiez un élément; vous pourriez potentiellement ignorer les éléments qui devaient être effacés.

C'est un algorithme courant que j'ai vu utilisé et documenté dans de nombreux endroits.

[EDIT] Vous avez raison de dire que les itérateurs sont invalidés après un effacement, mais seuls les itérateurs référençant l'élément qui est effacé, les autres itérateurs sont toujours valides. D'où l'utilisation iter++dans l' erase()appel.


4
Je suis confus; pourquoi utiliseriez-vous pour (; ...;) au lieu de while (...)? De plus, bien que cela fonctionne probablement, .erase ne renvoie-t-il pas un itérateur du suivant? Il semble donc que le blog if (Some Condition) devrait être iter = aMap.erase (iter) pour être le plus compatible. Peut-être que je manque quelque chose? Je n'ai pas l'expérience de certains d'entre vous.
taxilian

86
Notez qu'en C ++ 11, tous les conteneurs associatifs, y compris map, renvoient l'itérateur suivant à partir de erase(iter). C'est beaucoup plus propre à faire iter = erase( iter ).
Potatoswatter

10
@taxilian (années de retard) while () ou for () fonctionnerait, mais sémantiquement, les gens utilisent souvent for () pour itérer sur une plage connue, et while () pour un nombre inconnu de boucles. Puisque la plage est connue dans ce cas (du début à endIter ), for () ne serait pas un choix inhabituel, et serait probablement plus courant. Mais encore une fois, les deux seraient acceptables.
Jamin Gray

4
@taxilian Plus important encore: avec 'for', vous pouvez avoir la définition de votre itérateur DANS la portée de la boucle, afin qu'elle ne gâche pas le reste de votre programme.
Sanchises

1
@athos La question est formulée à la voix passive, "c'est recommandé". Il n'y a pas de recommandation universelle. Je pense que mon dernier commentaire est le moyen le plus simple. Cela implique deux copies de la variable itérateur, ce qui perd un peu d'efficacité comme quelqu'un l'a souligné ici. C'est votre appel qui vous convient.
Potatoswatter

75

erase_if pour std :: map (et autres conteneurs)

J'utilise le modèle suivant pour cette chose même.

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;
    }
  }
}

Cela ne retournera rien, mais supprimera les éléments de std :: map.

Exemple d'utilisation:

// '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 */;
});

Deuxième exemple (vous permet de passer une valeur de test):

// '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
});

3
@CodeAngry Merci - il m'a toujours semblé étrange que cela n'existait pas déjà std. Je comprends pourquoi il n'est pas membre de std::map, mais je pense que quelque chose comme ça devrait être dans la bibliothèque standard.
Iron Savior

3
Sera ajouté en C ++ 20 pourstd::map et autres.
Roi Danton


3

J'ai obtenu cette documentation de l' excellente référence SGI STL :

La carte a la propriété importante que l'insertion d'un nouvel élément dans une carte n'invalide pas les itérateurs qui pointent vers des éléments existants. L'effacement d'un élément d'une carte n'invalide pas non plus les itérateurs, à l'exception, bien sûr, des itérateurs qui pointent réellement vers l'élément en cours d'effacement.

Ainsi, l'itérateur que vous avez qui pointe vers l'élément à effacer sera bien sûr invalidé. Faites quelque chose comme ceci:

if (some condition)
{
  iterator here=iter++;
  aMap.erase(here)
}

3
Ce n'est pas différent du code d'origine. iter ++ incrémente l'itérateur puis retourne un itérateur pointant sur l'élément avant l'incrément.
Steve Folly

Mais iter ne sera pas invalidé puisque nous effaçons alors à la position d'ici
1800 INFORMATION

@ 1800INFORMATION: la saisie d'un appel de fonction est un point de séquence, l'effet secondaire d'incrément est évalué avant d' eraseêtre appelé. Ils sont donc effectivement équivalents. Pourtant, je préfère fortement votre version à l'original.
peterchen

Cela fonctionne pour un tableau ou un vecteur, mais provoquera un résultat inattendu dans la carte stl.
hunter_tech

2

Le code d'origine n'a qu'un seul problème:

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

Ici, le iterest incrémenté une fois dans la boucle for et une autre fois en effacement, ce qui aboutira probablement à une boucle infinie.


2

Voici une solution élégante.

for (auto it = map.begin(); it != map.end();)
{   
    (SomeCondition) ? map.erase(it++) : (++it);
}

1

D'après les notes de fond de:

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

un conteneur associatif de paire ne peut pas fournir d'itérateurs mutables (tels que définis dans les exigences de Trivial Iterator), car le type de valeur d'un itérateur mutable doit être Assignable, et la paire n'est pas Assignable. Cependant, un conteneur associatif de paire peut fournir des itérateurs qui ne sont pas complètement constants: des itérateurs tels que l'expression (* i) .second = d est valide.


1

Première

La carte a la propriété importante que l'insertion d'un nouvel élément dans une carte n'invalide pas les itérateurs qui pointent vers des éléments existants. L'effacement d'un élément d'une carte n'invalide pas non plus les itérateurs, à l'exception, bien sûr, des itérateurs qui pointent réellement vers l'élément en cours d'effacement.

Deuxièmement, le code suivant est bon

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

Lors de l'appel d'une fonction, les paramètres sont évalués avant l'appel de cette fonction.

Ainsi, quand iter ++ est évalué avant l'appel à effacer, l'opérateur ++ de l'itérateur retournera l'élément courant et pointera vers l'élément suivant après l'appel.


1

IMHO il n'y a pas d' remove_if()équivalent.
Vous ne pouvez pas réorganiser une carte. Vous ne pouvez
donc remove_if()pas mettre vos paires d'intérêt à la fin sur lesquelles vous pouvez appeler erase().


C'est vraiment dommage.
allyourcode

1

Basé sur la réponse d'Iron Savior Pour ceux qui aimeraient fournir une gamme plus proche des itérateurs fonctionnels std.

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;
    }
}

Curieux de savoir s'il existe un moyen de perdre les ContainerTéléments et de les obtenir de l'itérateur.


1
«Les identifiants commençant par un trait de soulignement suivi d'une lettre majuscule sont réservés pour toute utilisation par l'implémentation.»
YSC

0

Réponse de Steve Folly Je me sens le plus efficace.

Voici une autre solution simple mais moins efficace :

La solution utilise remove_copy_ifpour copier les valeurs souhaitées dans un nouveau conteneur, puis échange le contenu du conteneur d'origine avec celui du nouveau:

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);

2
Cela semble inefficace.
allyourcode

0

Si vous souhaitez effacer tous les éléments dont la clé est supérieure à 2, le meilleur moyen est

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

Fonctionne uniquement pour les plages, pas pour aucun prédicat.


0

J'utilise comme ça

 std::map<int, std::string> users;    
 for(auto it = users.begin(); it <= users.end()) {
    if(<condition>){
      it = users.erase(it);
    } else {
    ++it;
    }
 }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.