Unterschied zwischen leerem Destruktor und default Desktruktor
-
Habe gerade in einem Buch folgendes Beispiel gelesen
dachte immer, das sei dasselbe oO
btw, wie poste ich hier neuerdings Bilder oder Code?
-
Der Optimizer darf ja machen was er will, von daher kann man sich ehh nicht darauf verlassen. Der Code hat ja immer das richtige Resultat. Und wenn das Resultat das Gleiche ist, duerfte die Optimierung ja auch nicht verboten sein, wenn der Destruktor leer ist. D.h. Der Compiler ist nur zu bloed sie zu finden und es handelt sich gar nicht um eine allgemeine Aussage. Theoretisch kann es in deinem Compiler ja genau umgekehrt sein.
BTW: Code ist schoener.
-
Der Default destruktor wird AFAIK meist inline in der Klassen definition vom compiler definiert.
Ein leerer Destruktur (definiert in einer cpp datei) kann im folgenden Fall von Vorteil sein:
- Wenn man im Header als member der klasse einen std::unique_ptr<Foo> hat wobei Foo im header nur via forward declaration bekannt ist.
Im Falle des default Destruktors gibt es einen compiler fehler in jeder Übersetzungseinheit, welche diesen header inkludiert aber nicht die definition von Foo.
Da die Implementierung des std::unique_ptr<T> einen vollständige definierten Typ für das zerstören des Objektinstanz benötigtDurch die definition des leeren Destruktors in der cpp Datei, welche die Klassenimplementation enthält (welche Foo verwendet) kann man den compiler fehler lösen.
Da in diesem Kontext Foo vollständig definiert ist.
-
Die Optimierung wird aber ungleich schwieriger. Der leere Destruktor macht, dass die Klasse nicht mehr als POD zählt. Und das hat eben genau den Effekt, dass man eben nicht mehr Funktionen wie
memmove
anwenden darf.
-
@seppj Naja: Der Destruktor sagt hier weniger als die Member die verwendet werden. Ich habe x-Strukturen, die PODs sind und Konstruktoren haben, weil es einfacher ist.
Just my 2 cents
-
Konstruktoren dürfen sie IIRC laut aktuellem Standard eh haben. Aber halt keine Destruktoren.
-
@seppj sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
Die Optimierung wird aber ungleich schwieriger. Der leere Destruktor macht, dass die Klasse nicht mehr als POD zählt. Und das hat eben genau den Effekt, dass man eben nicht mehr Funktionen wie
memmove
anwenden darf.Ja, aber ist POD nicht einfach nur ein Name fuer etwas? Dann erfinde ich einfach einen neuen Namen POD1, und sage POD1 ist alles was POD ist plus alles was nur nicht POD ist weil es leeren Destruktor hat. Dann Optimierung auf POD1 anweden. Oder sehe ich das falsch?
Ich sehe da hoechstens das Problem, das etwas POD1 nicht korrekt zuordenbar ist, weil gerade die Implementation des Destruktors nicht bekannt ist, das ist aber im obigen Beispiel nicht der Fall.
-
Damit machst du die Optimierung ekelhaft schwer für den Compiler und daher ist das kein Wunder, dass diese normalerweise nicht geschieht. POD (oder genauer: Trivialer Konstruktor) kann ganz simpel per Templatemagie abgehandelt werden. Das ist die Implementation des std::copy in der Standardbibliothek, da muss der Optimierer im Compiler nicht einmal dran.
Beim anderen muss zuerst der Code für den copy-Loop erzeugt werden (wobei der Compiler dann sehen wird, dass er die sinnlosen Destruktoraufrufe weglassen kann), und dann muss hinterher umgekehrt erkannt werden, dass dieser Loop ohne die weggelassenen Destruktoraufrufe wieder einem memmove entspricht. Dazu ist tiefgehendes semantisches Verständnis des Codes notwendig. Viel Spaß, das zu optimieren.
-
@seppj: So wie ich es grad erklaert hatte, müsste ja genau das eben gar nicht passieren.
Ausserdem sehe ich auch nicht das Problem, wenn die Optimierung "schwer" ist, solange sie trotzdem gemacht wird - und daher halte ich diese generelle Ansage für mindestens fragwürdig. Habe aber jetzt auch keinen Test dazu gemacht.
-
ja ok, pimpl idion sehe ich ein als Ausnahmefall, aber nochmal zurück zu meinem eingangspost:
Wieso wird hier = default als remedy verkauft statt einem leeren Dtor?
-
@tggc sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
@seppj: So wie ich es grad erklaert hatte, müsste ja genau das eben gar nicht passieren.
Wieso? Deine Erklärung führt ganz exakt zu dem Problem, was ich beschrieben habe.
Noch einmal ganz ausführlich:
std::copy hat zwei Pfade, einen für POD und einen für non-POD. An der Stelle kann prinzipiell nur auf Existenz oder nicht-Existenz eines Destruktors geprüft werden, nicht auf Inhalt. Denn dies ist Templatemagie, keine Compileroptimierung. Der POD-Pfad führt zu einem expliziten memmove-Aufruf. Das geht, weil der Autor von std::copy ganz genau die Semantik der copy-Funktion kennt und weiß, dass die Funktionalität in diesem Fall äquivalent zu einem memmove ist. Der andere Pfad führt zu einer Schleife in der Art, wie sie im Beispiel des Threaderstellers benutzt wird. Danach greift der Optimierer und kann zum Beispiel leere Destruktoraufrufe in der Schleife wegoptimieren.Jetzt willst du ernsthaft einen Optimierer bauen, der for-Schleifen semantisch(!) analysiert und erkennt, dass die for-Schleife aus dem Beispiel durch ein memmove ersetzbar ist? Hast du irgendeine Ahnung, wie weit die Informatik davon weg ist? Viel Spaß beim Beweis, ob die Schleife überhaupt hält...
-
@sewing sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
ja ok, pimpl idion sehe ich ein als Ausnahmefall, aber nochmal zurück zu meinem eingangspost:
Wieso wird hier = default als remedy verkauft statt einem leeren Dtor?
Weil der default eben etwas anderes ist als ein expliziter, leerer Destruktor. Default (und keine Angabe) sagen ausdrücklich und unmissverständlich aus, dass es sich um den automatisch generierten Destruktor handelt (und das Objekt somit als POD handhabbbar ist). Beim leeren Destruktor musst du:
- Den Destruktor kennen. Das heißt, das geht nicht in Templates. Und es geht auch nicht, wenn der Destruktor in einer anderen Übersetzungseinheit steht. Das ist schon einmal sehr, sehr einschränkend, denn Templatemagie ist nicht so selten in modernem C++. Sogar recht häufig. Und verschiedene Übersetzungseinheiten sind auch sehr häufig.
- Dann musst du auch noch beweisen, dass der Destruktor nichts tut. Ok, das ist noch schaffbar, aber halt eine weitere Verkomplizierung, die dafür sorgt, dass das nur der Compiler machen kann und keine noch so clevere Implementierung auf Seite des Nutzers oder der Standardbibliothek. Und es gilt wieder, dass der Destruktor dafür auch vorliegen muss und nicht etwa in einer anderen Übersetzungseinheit liegt.
Wenn bekannt ist, dass der Destruktor der default-Destruktor ist, dann entfallen diese Schwierigkeiten komplett.
-
dann verstehe ich aber nicht, wieso in vielen Beispielen virtual ~Dtor() = default;
immer angeführt wird als Beispiel um zu zeigen, dass in diesem Fall keine move operationen vom compiler synthetisiert werden, wenn es doch offenbar in dem von mir genannten Beispiel doch funktioniert
-
Ja, aber wuerde es nicht reichen ein bool has_empty_destructor()<T> einzufuehren um das Problem zu loesen, oder nicht?
Das Unrollen einer Schleife mit konstant 64 Schritten wird übrigens von einigen Compilern gemacht, wir sind nicht also auch nicht soweit davon entfernt.
-
@tggc sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
Ja, aber wuerde es nicht reichen ein bool has_empty_destructor()<T> einzufuehren um das Problem zu loesen, oder nicht?
Wie soll das funktionieren? Soll das Template den Code inspizieren?
Das Unrollen einer Schleife mit konstant 64 Schritten wird übrigens von einigen Compilern gemacht, wir sind nicht also auch nicht soweit davon entfernt.
Es ist auch ungleich einfacher zu erkennen, wie oft eine Schleife läuft, als zu erkennen, was der Unterschied des Programmzustands zwischen Beginn und Abschluss einer Schleife ist. Selbst wenn wir mal Aliasing raus lassen (was dies noch einmal unendlich viel schwerer machen würde und daher vom Sprachstandard quasi wegdefiniert wurde), müsste der Optimierer erkennen, dass ein Stück Code den gleichen Effekt hat wie memmove. Soll ich dir hier ein Dutzend Codeschnipsel zeigen und du sagst mir dazu, ob diese exakt durch eine bekannte C-Funktion ersetzbar sind und wenn ja, durch welche? Mal schauen, wie schwer sich ein Mensch dabei tut.
-
@sewing sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
dann verstehe ich aber nicht, wieso in vielen Beispielen virtual ~Dtor() = default;
immer angeführt wird als Beispiel um zu zeigen, dass in diesem Fall keine move operationen vom compiler synthetisiert werden, wenn es doch offenbar in dem von mir genannten Beispiel doch funktioniertHolla! Zwischen dem default-erzeugten Destruktor und einem virtuellen default-erzeugten Destruktor ist ein Riesenunterschied. virtual und POD passt so überhaupt gar nicht zusammen. Der Compiler muss hier schließlich Code für die copy-Funktion erzeugen, der auch noch den vtable updated (oder wie auch immer der Compiler die Virtualität implementiert). Wie du bei godbolt sehen kannst, ist das sogar noch eine Stufe mehr Code als nur mit leerem Destruktor. memmove ist da offensichtlich komplett ausgeschlossen.
-
@seppj sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
@sewing sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
ja ok, pimpl idion sehe ich ein als Ausnahmefall, aber nochmal zurück zu meinem eingangspost:
Wieso wird hier = default als remedy verkauft statt einem leeren Dtor?
Weil der default eben etwas anderes ist als ein expliziter, leerer Destruktor. Default (und keine Angabe) sagen ausdrücklich und unmissverständlich aus, dass es sich um den automatisch generierten Destruktor handelt (und das Objekt somit als POD handhabbbar ist). Beim leeren Destruktor musst du:
- Den Destruktor kennen. Das heißt, das geht nicht in Templates.
Weshalb? (Bzw. wie meinst Du das?)
- Dann musst du auch noch beweisen, dass der Destruktor nichts tut. Ok, das ist noch schaffbar, aber halt eine weitere Verkomplizierung, die dafür sorgt, dass das nur der Compiler machen kann und keine noch so clevere Implementierung auf Seite des Nutzers oder der Standardbibliothek.
Wir sprechen von einer Optimierung. Der absolute Großteil aller Optimierungen basiert auf Informationen die der Compiler durch dataflow/controlflow/etc. analysis herleitet.
Und es gilt wieder, dass der Destruktor dafür auch vorliegen muss und nicht etwa in einer anderen Übersetzungseinheit liegt.
Ein leerer Destruktor wird selten in einer anderen Übersetzungseinheit liegen, aber uns muss dieser Fall auch gar nicht kümmern.
Wenn bekannt ist, dass der Destruktor der default-Destruktor ist, dann entfallen diese Schwierigkeiten komplett.
Das bedeutet nicht, dass ein Compiler keinen Anreiz hat, ein gängiges Anti-Idiom wie eine leere Definition von Destruktoren zu berücksichtigen. Weil es, grob gesagt, immer noch um Amdahl's law geht. Wenn genug Trottel solchen Code schreiben, wird er Compiler einfach eine entsprechende, hardgecodete Abfrage integrieren wollen. Warum versuchst Du, dieses Problem mit viel schwereren, allgemein unentscheidbaren zu assoziieren? Ein leerer Destruktor kann syntaktisch erkannt und in einem Kontext erfasst werden, welcher dann dem Optimierer zur Verfügung steht. Wer hat von nicht-trivialen Beispielen gesprochen?
@seppj sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
@tggc sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
Ja, aber wuerde es nicht reichen ein bool has_empty_destructor()<T> einzufuehren um das Problem zu loesen, oder nicht?
Wie soll das funktionieren? Soll das Template den Code inspizieren?
Ich verstehe den Einwand nicht. Es wurden schon einige Traits (in der stdlib) eingeführt, die nicht mittels der Sprache allein implementiert werden können.
-
Ich habe es jetzt schon 2x genau erklärt. Ich erkläre es nicht noch einmal, bloß weil ihr die objektive Realität nicht akzeptieren wollt.
-
Du willst offenbar nicht zwischen Durchgeführtem und Durchführbarkeit differenzieren. Dass Implementierungen Destruktoren ohne Effekt, die als
{}
definiert wurden, nicht ignorieren, bedeutet nicht, dass das schwer wäre. Sondern dass bislang kein Anreiz dafür bestand. Ich wollte hier auch lediglich erwähnen, dass die Basisfälle (wie eben{}
) völlig unproblematisch sind, und auch eine gute Portion aller realen Fälle von effektfreien Destruktoren ausmachen. Mir (und @TGGC) ist schon klar, dass Compiler nicht blöd genug sind, aussichtslose Analysen laufen zu lassen.
-
@c-olumbo sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
Du willst offenbar nicht zwischen Durchgeführtem und Durchführbarkeit differenzieren. Dass Implementierungen Destruktoren ohne Effekt, die als
{}
definiert wurden, nicht ignorieren, bedeutet nicht, dass das schwer wäre. Sondern dass bislang kein Anreiz dafür bestand. Ich wollte hier auch lediglich erwähnen, dass die Basisfälle (wie eben{}
) völlig unproblematisch sind, und auch eine gute Portion aller realen Fälle von effektfreien Destruktoren ausmachen. Mir (und @TGGC) ist schon klar, dass Compiler nicht blöd genug sind, aussichtslose Analysen laufen zu lassen.Du willst nicht die Schwierigkeit erkennen! Zu der Zeit, wo es einfach ist, die Optimierung durchzuführen, ist es nahezu unmöglich schwer, die nötigen Voraussetzungen zu erkennen. Und zu der Zeit, wo es einfach ist, die nötigen Voraussetzungen zu erkennen, ist es nahezu unmöglich schwer, die Optimierung durchzuführen. Da kannst du auch nicht einfach so etwas dran ändern, ohne die ganze Sprache komplett umzukrempeln, weil die Ursachen tief darin verankert sind, wie die Sprache definiert ist und welche Folgen dies dafür hat, wie die Verarbeitungsreihenfolge bei der Übersetzung ist (und dass die Sprache so definiert ist, hat wiederum Gründe darin, wie man besonders gut Übersetzer schreiben kann). Du kannst nicht einfach die Realität umdefinieren, weil sie dir nicht gefällt!
Ich habe nun schon sehr oft die genaue Erklärung wiederholt, jedes weitere "Warum?" verweise ich auf meine vorherigen Erklärungen. Du kannst den Fakt, dass es so ist, ganz einfach mit jedem Compiler nachvollziehen. Meine schlüssige Erklärung kannst du akzeptieren oder eben den Rest der Menschheit für blöd erklären, weil sie in Jahrzehnten von Optimierungsforschung nicht auf die Lösung gekommen ist, die du dir heute Nachmittag ausgedacht hast.
@c-olumbo sagte in Unterschied zwischen leerem Destruktor und default Desktruktor:
Ich verstehe den Einwand nicht. Es wurden schon einige Traits (in der stdlib) eingeführt, die nicht mittels der Sprache allein implementiert werden können.
Nenn ein einziges Trait, das Codeinspektion voraussetzt. Es gibt kein einziges. Wenn du verstehst warum, dann hast du verstanden, wieso dein angeblich so einfaches Vorhaben so schwer ist. Sämtliche Traits beziehen sich auf die Definitionen und Eigenschaften von Datenstrukturen, die keine Realität im späteren Maschinencode haben.
Es gibt ein paar Traits, die ein bisschen in die Richtung Inspektion gehen, wie die is_nothrow_Xable. Wie du aber merken wirst, beziehen diese sich bloß auf die explizite Markierung der untersuchten Objekte mittels
noexcept
ähnlichem. Also Versprechen, die der Programmierer an dieser Stelle machen kann, damit Optimierungen frühzeitig durchgeführt werden können, wo es Sinn macht, und der Compiler kann dann bei einem späteren Durchgang prüfen, ob das Versprechen auch gehalten wurde. Aber umgekehrt kann der Compiler nicht frühzeitig sagen, ob etwas noexcept ist, außer der Programmierer gibt ihm dieses Versprechen. Das =default geht in die gleiche Richtung und tatsächlich gibt es ja auch jede Menge Traits, die sich darauf beziehen.