C ++ - Iteratorlebensdauer und Erkennen einer Ungültigkeit

8

Basierend auf dem, was in C ++ 11 als idiomatisch angesehen wird:

  • Sollte ein Iterator in einem benutzerdefinierten Container den zerstörten Container selbst überleben?
  • sollte es möglich sein zu erkennen, wann ein Iterator ungültig wird?
  • Sind die oben genannten Bedingungen in der Praxis von "Debug-Builds" abhängig?

Details : Ich habe kürzlich mein C ++ aufgefrischt und mich in C ++ 11 zurechtgefunden. Als Teil davon habe ich einen idiomatischen Wrapper um die Uriparser-Bibliothek geschrieben . Ein Teil davon ist das Umschließen der verknüpften Listendarstellung von analysierten Pfadkomponenten. Ich suche Rat, was für Container idiomatisch ist.

Eine Sache, die mich beunruhigt, da ich zuletzt aus durch Müll gesammelten Sprachen stamme, ist sicherzustellen, dass zufällige Objekte nicht nur bei Benutzern verschwinden, wenn sie einen Fehler in Bezug auf die Lebensdauer machen. Um dies zu berücksichtigen, PathListbehalten sowohl der Container als auch seine Iteratoren ein shared_ptrObjekt für den tatsächlichen internen Status bei. Dies stellt sicher, dass, solange etwas vorhanden ist, das auf diese Daten hinweist, auch die Daten vorhanden sind.

Wenn man sich jedoch die STL (und viele Suchanfragen) ansieht , sieht es nicht so aus, als würden C ++ - Container dies garantieren. Ich habe den schrecklichen Verdacht, dass die Erwartung besteht, Container einfach zerstören zu lassen und damit alle Iteratoren ungültig zu machen. std::vectorscheint sicherlich zuzulassen, dass Iteratoren ungültig werden und trotzdem (falsch) funktionieren.

Was ich wissen möchte ist: Was wird von "gutem" / idiomatischem C ++ 11-Code erwartet? Angesichts der glänzenden neuen intelligenten Zeiger erscheint es seltsam, dass Sie mit STL Ihre Beine leicht abblasen können, indem Sie versehentlich einen Iterator auslaufen lassen. Ist die Verwendung shared_ptrder Sicherungsdaten eine unnötige Ineffizienz, eine gute Idee zum Debuggen oder etwas, das von STL einfach nicht erwartet wird?

(Ich hoffe, dass durch die Erdung auf "idiomatisches C ++ 11" Anklagen wegen Subjektivität vermieden werden ...)

DK.
quelle

Antworten:

10

Ist die Verwendung shared_ptrder Hintergrunddaten eine unnötige Ineffizienz

Ja - es erzwingt eine zusätzliche Indirektion und eine zusätzliche Zuordnung pro Element, und in Multithread-Programmen ist jedes Inkrementieren / Dekrementieren des Referenzzählers besonders teuer, selbst wenn ein bestimmter Container nur innerhalb eines einzelnen Threads verwendet wird.

All dies mag in manchen Situationen in Ordnung und sogar wünschenswert sein, aber die allgemeine Regel besteht darin, keine unnötigen Gemeinkosten aufzuerlegen, die der Benutzer nicht vermeiden kann , selbst wenn sie nutzlos sind.

Da keiner dieser Overheads notwendig ist, sondern eher das Debuggen von Feinheiten (und denken Sie daran, dass eine falsche Iteratorlebensdauer ein statischer Logikfehler ist, kein seltsames Laufzeitverhalten), würde sich niemand bei Ihnen dafür bedanken, dass Sie den richtigen Code verlangsamt haben , um Ihre Fehler zu erkennen.


Also zur ursprünglichen Frage:

Sollte ein Iterator in einem benutzerdefinierten Container den zerstörten Container selbst überleben?

Die eigentliche Frage ist, ob die Kosten für die Verfolgung aller Live-Iteratoren in einem Container und deren Ungültigmachung bei Zerstörung des Containers Personen auferlegt werden sollten, deren Code korrekt ist.

Ich denke wahrscheinlich nicht, obwohl, wenn es einen Fall gibt, in dem es wirklich schwierig ist, die Lebensdauer von Iteratoren korrekt zu verwalten und Sie bereit sind, den Treffer zu erzielen, ein dedizierter Container (oder Containeradapter), der diesen Service bereitstellt, als Option hinzugefügt werden könnte .

Alternativ mag der Wechsel zu einer Debug-Implementierung basierend auf einem Compiler-Flag sinnvoll sein, aber es ist eine viel größere und teurere Änderung als die meisten, die von DEBUG / NDEBUG gesteuert werden. Es ist sicherlich eine größere Änderung, als entweder Assert-Anweisungen zu entfernen oder einen Debugging-Allokator zu verwenden.


Ich habe vergessen zu erwähnen, aber Ihre Lösung, shared_ptrüberall zu verwenden, behebt Ihren Fehler ohnehin nicht unbedingt: Sie kann ihn lediglich gegen einen anderen Fehler austauschen , nämlich einen Speicherverlust.

Nutzlos
quelle
"Sollten die Kosten für die Verfolgung aller Live-Iteratoren in einem Container und deren Ungültigmachung bei Zerstörung des Containers Personen auferlegt werden, deren Code korrekt ist?" zum Teufel überhaupt nicht . Wie aus Ihrem Beitrag hervorgeht, lautet eines der De-facto- Mottos von C ++ "Sie zahlen nicht für das, was Sie nicht verwenden". Dies hat einen sehr guten Grund: Es würde viele gut programmierte Projekte lahm legen, wenn sie Sinnesprüfungen gegen all die dummen Dinge durchführen müssten, die ein schlechter Programmierer tun könnte. Aber natürlich, wie Sie angedeutet haben, wenn jemand das wirklich will ... hat er die Werkzeuge, um es selbst zu implementieren (und zu behalten). Beste aus beiden Welten!
underscore_d
7

Wenn Sie in C ++ den Container zerstören lassen, werden die Iteratoren ungültig. Zumindest bedeutet dies, dass der Iterator nutzlos ist, und wenn Sie versuchen, ihn zu dereferenzieren, können viele schlechte Dinge passieren (genau wie schlecht hängt von der Implementierung ab, aber es ist normalerweise ziemlich schlecht).

In einer Sprache wie C ++ liegt es in der Verantwortung des Programmierers, solche Dinge klar zu halten. Das ist eine der Stärken der Sprache, denn Sie können sich ziemlich darauf verlassen, wann etwas passiert (Sie haben ein Objekt gelöscht? Das bedeutet, dass zum Zeitpunkt des Löschens der Destruktor aufgerufen und der Speicher freigegeben wird und Sie sich darauf verlassen können dazu), aber es bedeutet auch, dass Sie Iteratoren nicht überall in Containern aufbewahren und diesen Container dann löschen können.

Könnten Sie jetzt einen Container schreiben, der die Daten so lange aufbewahrt, bis alle Iteratoren verschwunden sind? Natürlich haben Sie das klar im Griff. Das ist NICHT die übliche C ++ - Methode, aber es ist nichts Falsches daran, solange es ordnungsgemäß dokumentiert (und natürlich debuggt) ist. So funktionieren die STL-Container einfach nicht.

Michael Kohne
quelle
1
Beachten Sie, dass schlecht von der Rückkehr eines Sentinals zu undefiniertem Verhalten gehen kann
Ratschenfreak
@ratchetfreak - ja, das stimmt. In dem fraglichen Fall (Iteratoren in einen Container) gibt es normalerweise keine gute Möglichkeit, den Sentininalwert zu definieren, daher tendiert die übliche C ++ - Methode (und das Verhalten der STL) zu "undefiniertem Verhalten".
Michael Kohne
5

Einer der (oft nicht genannten) Unterschiede zwischen C ++ - und GC-Sprachen besteht darin, dass die gängige C ++ - Sprache davon ausgeht, dass alle Klassen Wertklassen sind.

Es gibt Zeiger und Referenzen, aber sie sind meistens verbannt, wenn es darum geht, polymorphen Versand (über die Indirektion virtueller Funktionen) zuzulassen oder Objekte zu verwalten, deren Lebensdauer die des Blocks überleben muss, der sie erstellt hat.

In diesem letzten Fall liegt es in der Verantwortung des Programmierers, die Politik und Politik darüber zu definieren, wer schafft und wer und wann zerstören muss. Intelligente Zeiger (wie shared_ptroder unique_ptr) sind nur Werkzeuge, die bei dieser Aufgabe in ganz bestimmten (und häufigen) Fällen helfen, in denen ein Objekt von verschiedenen Eigentümern "geteilt" wird (und Sie möchten, dass das letzte Objekt es zerstört) oder kontextübergreifend verschoben werden müssen immer einen einzigen Kontext haben, der es besitzt.

Interatoren sind von Natur aus nur während ... einer Iteration sinnvoll, und daher sollten sie nicht "zur späteren Verwendung gespeichert" werden, da das, worauf sie sich beziehen, nicht gewährt wird, um gleich zu bleiben oder dort zu bleiben (ein Container kann es verschieben Inhalt beim Wachsen oder Schrumpfen ... alles ungültig machen). Linkbasierte Container (wie lists) sind eine Ausnahme von dieser allgemeinen Regel, nicht die Regel selbst.

Wenn in der idiomatischen C ++ A B "braucht", muss B an einem Ort sein, der länger lebt als der Ort, an dem A ist, daher ist keine "Lebensverfolgung" von B von A erforderlich.

shared_ptrund weak_ptrhelfen Sie, wenn diese Redewendung zu restriktiv ist, indem Sie jeweils die Richtlinien "Gehen Sie nicht weg, bis wir alle es zulassen" oder die Richtlinien "Wenn Sie weggehen, hinterlassen Sie uns einfach eine Nachricht" zulassen. Aber sie haben Kosten, da sie dazu einige Hilfsdaten zuweisen müssen.

Der nächste Schritt sind gc_ptr-s (die die Standardbibliothek nicht bietet, die Sie jedoch implementieren können, wenn Sie möchten, beispielsweise mit Mark & ​​Sweep-Algorithmen), bei denen die Tracking-Strukturen noch komplexer und prozessorintensiver sind ihre Wartung.

Emilio Garavaglia
quelle
4

In C ++ ist es idiomatisch, irgendetwas davon zu machen

  • kann durch sorgfältige Codierung verhindert werden und
  • würde Laufzeitkosten zum Schutz verursachen

ein undefiniertes Verhalten .

Insbesondere bei Iteratoren wird in der Dokumentation der einzelnen Container angegeben, welche Operationen Iteratoren ungültig machen (die Zerstörung des Containers gehört immer dazu), und der Zugriff auf ungültige Iteratoren ist undefiniertes Verhalten. In der Praxis bedeutet dies, dass die Laufzeit blind auf den nicht mehr gültigen Zeiger zugreift. Normalerweise stürzt es ab, aber es kann den Speicher beschädigen und zu völlig unvorhersehbaren Ergebnissen führen.

Es wird empfohlen, optionale Überprüfungen bereitzustellen, die im Debug-Modus aktiviert werden können ( #definestandardmäßig aktiviert, wenn _DEBUGdefiniert und deaktiviert, wenn dies der Fall NDEBUGist).

Denken Sie jedoch daran, dass C ++ für Fälle ausgelegt ist, in denen jede Leistung benötigt wird und die Überprüfungen manchmal recht kostspielig sein können, da Iteratoren häufig in engen Schleifen verwendet werden. Aktivieren Sie sie daher nicht standardmäßig.

In unserem Arbeitsprojekt musste ich die Iteratorprüfung in der Microsoft-Standardbibliothek auch im Debug-Modus deaktivieren, da einige Container andere Container und Iteratoren intern verwenden und die Zerstörung eines großen Containers aufgrund der Prüfungen eine halbe Stunde dauerte!

Jan Hudec
quelle