Das Update von C ++ std :: set ist mühsam: Ich kann kein Element an Ort und Stelle ändern

71

Ich finde den Update-Vorgang std::setmühsam, da es bei cppreference keine solche API gibt . Was ich derzeit mache, ist ungefähr so:

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

Grundsätzlich ist der Iterator return by Seta const_iteratorund Sie können seinen Wert nicht direkt ändern.

Gibt es einen besseren Weg, dies zu tun? Oder vielleicht sollte ich es überschreiben, std::setindem ich mein eigenes erstelle (was ich nicht genau weiß, wie es funktioniert ..)

Figo
quelle
3
Erstellen Sie eine Inline-Funktion, wenn die Verwendung von 2 Anweisungen bereits mühsam ist.
Kennytm
KennyTM traf den Nagel auf den Kopf. Es gibt keine Nachteile in Bezug auf die Leistung, also machen Sie es einfach schon! :-P
j_random_hacker
1
Wenn Sie eine Aktualisierungsfunktion schreiben, möchten Sie diese möglicherweise auf die gleiche Weise wie Boost.MultiIndex modellieren: boost.org/doc/libs/release/libs/multi_index/doc/tutorial/…
Emile Cormier
1
cplusplus.com ist eine schreckliche Referenz. Aspekte der Sprache "langweilig" zu finden, scheint ... seltsam.
Leichtigkeitsrennen im Orbit
1
Ich denke, der Nachteil dieses Ansatzes ist die Verwendung einer Lese- / Schreibsperre bei gleichzeitiger Verarbeitung.
UchihaItachi

Antworten:

77

setRückkehr const_iterators(der Standard sagt set<T>::iteratorist const, und dass set<T>::const_iteratorund set<T>::iteratorin der Tat kann die gleiche Art sein - siehe 23.2.4 / 6 in n3000.pdf) , weil es ein geordnete Container ist. Wenn ein regulärer iteratorWert zurückgegeben wird, können Sie den Artikelwert unter dem Container ändern und möglicherweise die Reihenfolge ändern.

Ihre Lösung ist die idiomatische Möglichkeit, Elemente in a zu ändern set.

Terry Mahaffey
quelle
Mitglieder von (non-const) std::setkehren nicht zurück const_iteratorund Sie können die Elemente ändern, wenn Sie vorsichtig sind. Warum bekommt diese (falsche) Antwort so viele positive Stimmen? Was vermisse ich?
Avakar
1
Meine Antwort ist richtig - deine ist falsch. Ich habe meinen Beitrag mit Verweisen auf den Standard aktualisiert.
Terry Mahaffey
6
Terry, danke für die Diskussion. Ich habe noch einmal überprüft: Der Fehlerbericht wurde zwar 1998 eingereicht, aber nicht in C ++ 03 aufgenommen. Es wird in C ++ 0x sein. Während Ihre Antwort in Bezug auf den aktuellen Buchstaben des Standards nicht korrekt ist, ist sie in Bezug auf die Absicht korrekt. +1.
Avakar
Vielen Dank für Ihren Kommentar und Ihre Debatte mit Avakar (oder Avatar?). Sie haben viel geholfen.
Figo
2
@avakar: Die Elemente sind technisch veränderbar, dürfen aber nicht geändert werden. Dies ist ein Fehler in der Norm.
Leichtigkeitsrennen im Orbit
27

Im einfachen Fall gibt es zwei Möglichkeiten:

  • Sie können mutablefür die Variable verwenden, die nicht Teil des Schlüssels sind
  • Sie können Ihre Klasse in ein Key ValuePaar aufteilen (und a verwenden std::map)

Die Frage ist nun für den schwierigen Fall: Was passiert, wenn das Update tatsächlich den keyTeil des Objekts ändert ? Ihr Ansatz funktioniert, obwohl ich zugebe, dass es langweilig ist.

Matthieu M.
quelle
3
Das Hinzufügen von veränderlichen Elementen zu einem Nicht-Schlüssel-Mitglied ist möglicherweise in Ordnung, wenn es sich um eine interne / private Klasse handelt, aber es ist immer noch ein schmutziger Hack! Sobald die Klasse einigen Benutzern ausgesetzt ist, darf id niemals mutable für Mitglieder verwenden, die nicht mutable sein sollen! Das ist böse!
Marti Nito
14

In C ++ 17 können Sie extract()dank P0083 besser damit umgehen :

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

Dies vermeidet eine zusätzliche Kopie Ihres Typs und eine zusätzliche Zuordnung / Freigabe und funktioniert auch mit Nur-Verschieben-Typen.

Sie können auch extractper Schlüssel. Wenn der Schlüssel fehlt, wird ein leerer Knoten zurückgegeben:

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

Update: Obwohl das Folgende ab sofort zutrifft, wird das Verhalten als Fehler angesehen und in der kommenden Version des Standards geändert. Wie sehr traurig.


Es gibt mehrere Punkte, die Ihre Frage ziemlich verwirrend machen.

  1. Funktionen können Werte zurückgeben, Klassen nicht. std::setist eine Klasse und kann daher nichts zurückgeben.
  2. Wenn Sie anrufen können s.erase(iter), dann iterist kein const_iterator. eraseerfordert einen nicht konstanten Iterator.
  3. Alle Mitgliedsfunktionen std::setdavon geben einen Iterator zurück, der einen nicht konstanten Iterator zurückgibt, solange die Menge ebenfalls nicht konstant ist.

Sie können den Wert eines Elements einer Menge ändern, solange die Aktualisierung die Reihenfolge der Elemente nicht ändert. Der folgende Code wird kompiliert und funktioniert einwandfrei.

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

Wenn Ihr Update die Reihenfolge der Elemente ändert, müssen Sie es löschen und erneut einfügen.

Avakar
quelle
1
Nun, ich schaue auf 23.1.2 [lib.associative.reqmts] von C ++ 03, Tabelle 69 und es heißt: "a.find (k): iterator; const_iterator für Konstante a".
Avakar
1
Ich habe das C ++ 03 PDF nicht zur Hand (es kostet Geld). Ich dachte, dies wurde in C ++ 03 behoben, aber es könnte ein Post-03-Fix gewesen sein. In n3000.pdf ist es 23.2.4 / 6, was erklärt, dass der Iterator für assoziative Container ein konstanter Iterator ist (und vom gleichen Typ wie const_iterator sein kann). Welchen Compiler verwenden Sie? Dieses Verhalten wurde auch in VC9 implementiert (das C ++ 03 verfolgte), weshalb ich dachte, das Verhalten sei eine C ++ 03-Fehlerbehebung.
Terry Mahaffey
1
Schauen Sie nicht auf die Tabelle, sondern auf den Absatz, in dem die Anforderungen von "Iterator" an einen assoziativen Container erläutert werden. Ein Iterator kann "const" sein, ohne tatsächlich "const_iterator" genannt zu werden.
Terry Mahaffey
4
Es befindet sich in den Standardbibliotheksfehlerberichten: open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#103 . Es wird nicht mit GCC kompiliert, wodurch DR 103 erwähnt wird und typedefs sowohl Iterator als auch const_iterator vom selben Typ sind. - Eine vorgeschlagene Problemumgehung für die Probleme von OP, BTW, sind const_cast und veränderbare Mitglieder.
OnkelBens
1
@ OnkelBens, ja, ich habe auch die DR gefunden. Ich bin ein wenig überrascht, dass es nicht in C ++ 03 geschafft hat, sondern in den aktuellen Entwurf aufgenommen wurde. Aus meiner Sicht ist dies eine ziemlich schwerwiegende Änderung, da sonst der korrekte Code beschädigt wird.
Avakar
8

Möglicherweise möchten Sie std::mapstattdessen eine verwenden. Verwenden Sie den Teil, der Elementsich auf die Bestellung des Schlüssels auswirkt, und geben Sie alles Elementals Wert ein. Es wird einige kleinere Daten duplizieren, aber Sie werden einfachere (und möglicherweise schnellere) Updates haben.

Dev Null
quelle
2

Ich bin auf dasselbe Problem in C ++ 11 gestoßen, wo es tatsächlich ::std::set<T>::iteratorkonstant ist und daher keine Änderung des Inhalts zulässt, selbst wenn wir wissen, dass die Transformation die <Invariante nicht beeinflusst . Sie können dies umgehen, indem Sie ::std::seteinen mutable_setTyp einschließen oder einen Wrapper für den Inhalt schreiben:

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

Ich finde das viel einfacher und es funktioniert in 90% der Fälle, ohne dass der Benutzer bemerkt, dass zwischen dem Set und dem tatsächlichen Typ etwas liegt.

Bitmaske
quelle
Interessante Idee, ich werde versuchen, mich an diese zu erinnern.
Mark Ransom
2
@MarkRansom: Ja, aber um ganz sicher zu gehen, sollte beachtet werden, dass dies nur verwendet werden darf, wenn durch die Änderung der hinter dem Iterator gespeicherten Daten garantiert wird , dass die Reihenfolge des Satzes nicht geändert wird. Andernfalls ist dies UB und es wird brechen! (Ich wiederhole dies nur, um sicherzugehen, dass sich niemand in den Fuß schießt. Ich impliziere nicht, dass Sie dies nicht realisieren.)
Bitmaske
+1 Dies ist ideal, wenn Sie eindeutige sortierte Elemente basierend auf einem Schlüssel (für welches Set der entsprechende Container ist) möchten, aber auch einige 'Metadaten' bei sich behalten möchten, die sich ändern können
am
Dieser Ansatz funktioniert bei meinem Objekt nicht. Speziell für den Zeiger-ähnlichen Operator "operator -> ()", wenn ich versuche, meine Klassenmethode aufzurufen.
Erman
0

Dies ist in einigen Fällen schneller:

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

Wenn der Wert normalerweise noch nicht im Wert ist, std::setkann dies eine bessere Leistung haben.

jcoffland
quelle