web-dev-qa-db-fra.com

La mise à jour C ++ std :: set est fastidieuse: je ne peux pas changer un élément en place

Je trouve l'opération de mise à jour sur std::set fastidieux car il n'y a pas une telle API sur cppreference . Donc, ce que je fais actuellement, c'est quelque chose comme ceci:

//find element in set by iterator
Element copy = *iterator;
... // update member value on copy, varies
Set.erase(iterator);
Set.insert(copy);

Fondamentalement, le retour de l'itérateur par Set est un const_iterator et vous ne pouvez pas modifier sa valeur directement.

Y a-t-il une meilleure manière de faire cela? Ou peut-être que je devrais remplacer std::set en créant le mien (dont je ne sais pas exactement comment ça marche ..)

68
Figo

set renvoie const_iterators (la norme indique set<T>::iterator est const, et que set<T>::const_iterator et set<T>::iterator peut en fait être du même type - voir 23.2.4/6 dans n3000.pdf) car il s'agit d'un conteneur commandé. S'il renvoyait un iterator normal, vous seriez autorisé à modifier la valeur des articles sous le conteneur, ce qui pourrait modifier la commande.

Votre solution est le moyen idiomatique de modifier des éléments dans un set.

72
Terry Mahaffey

Il y a 2 façons de procéder, dans le cas simple:

  • Vous pouvez utiliser mutable sur la variable qui ne fait pas partie de la clé
  • Vous pouvez diviser votre classe en KeyValue paire (et utiliser un std::map)

Maintenant, la question est pour le cas délicat: que se passe-t-il lorsque la mise à jour modifie réellement la partie key de l'objet? Votre approche fonctionne, même si j'avoue que c'est fastidieux.

22
Matthieu M.

En C++ 17, vous pouvez faire mieux avec extract() , grâce à P008 :

// remove element from the set, but without needing
// to copy it or deallocate it
auto node = Set.extract(iterator);
// make changes to the value in place
node.value() = 42;
// reinsert it into the set, but again without needing 
// to copy or allocate
Set.insert(std::move(node));

Cela évitera une copie supplémentaire de votre type et une allocation/désallocation supplémentaire, et fonctionnera également avec les types à déplacement uniquement.

Vous pouvez également extract par clé. Si la clé est absente, cela renverra un nœud vide:

auto node = Set.extract(key);
if (node) // alternatively, !node.empty()
{
    node.value() = 42;
    Set.insert(std::move(node));
}
8
Barry

Mise à jour: Bien que ce qui suit soit vrai à partir de maintenant, le comportement est considéré comme défaut et sera modifié dans la prochaine version de la norme. C'est très triste.


Il y a plusieurs points qui rendent votre question assez confuse.

  1. Les fonctions peuvent renvoyer des valeurs, les classes non. std::set Est une classe et ne peut donc rien renvoyer.
  2. Si vous pouvez appeler s.erase(iter), alors iter n'est pas un const_iterator. erase nécessite un itérateur non const.
  3. Toutes les fonctions membres de std::set Qui renvoient un itérateur renvoient un itérateur non const tant que l'ensemble est également non const.

Vous êtes autorisé à modifier la valeur d'un élément d'un ensemble tant que la mise à jour ne modifie pas l'ordre des éléments. Le code suivant compile et fonctionne très bien.

#include <set>

int main()
{
    std::set<int> s;
    s.insert(10);
    s.insert(20);

    std::set<int>::iterator iter = s.find(20);

    // OK
    *iter = 30;

    // error, the following changes the order of elements
    // *iter = 0;
}

Si votre mise à jour modifie l'ordre des éléments, vous devez alors effacer et réinsérer.

8
avakar

Vous pouvez utiliser un std::map au lieu. Utilisez la partie de Element qui affecte l'ordre de la clé et mettez tout Element comme valeur. Il y aura quelques duplications mineures de données, mais vous aurez des mises à jour plus faciles (et peut-être plus rapides).

7
user268238

J'ai rencontré le même problème en C++ 11, où en effet ::std::set<T>::iterator est constant et ne permet donc pas de changer son contenu, même si l'on sait que la transformation n'affectera pas le < invariant. Vous pouvez contourner ce problème en enveloppant ::std::set dans une mutable_set tapez ou écrivez un wrapper pour le contenu:

  template <typename T>
  struct MutableWrapper {
    mutable T data;
    MutableWrapper(T const& data) : data(data) {}
    MutableWrapper(T&& data) : data(data) {}
    MutableWrapper const& operator=(T const& data) { this->data = data; }
    operator T&() const { return data; }
    T* operator->() const { return &data; }
    friend bool operator<(MutableWrapper const& a, MutableWrapper const& b) {
      return a.data < b.data;
    }   
    friend bool operator==(MutableWrapper const& a, MutableWrapper const& b) {
      return a.data == b.data;
    }   
    friend bool operator!=(MutableWrapper const& a, MutableWrapper const& b) {
      return a.data != b.data;
    }   
  };

Je trouve cela beaucoup plus simple et cela fonctionne dans 90% des cas sans que l'utilisateur ne remarque qu'il y ait quelque chose entre l'ensemble et le type réel.

2
bitmask

C'est plus rapide dans certains cas:

std::pair<std::set<int>::iterator, bool> result = Set.insert(value);
if (!result.second) {
  Set.erase(result.first);
  Set.insert(value);
}

Si la valeur n'est généralement pas déjà dans le std::set alors cela peut avoir de meilleures performances.

0
jcoffland