Betrachten Sie die folgende Implementierung einer einzeln verknüpften Liste:
struct node {
std::unique_ptr<node> next;
ComplicatedDestructorClass data;
}
Angenommen, ich verwende std::unique_ptr<node> head
keine Instanz mehr, die dann außerhalb des Gültigkeitsbereichs liegt und deren Destruktor aufgerufen wird.
Wird dies meinen Stapel für ausreichend große Listen sprengen? Ist es fair anzunehmen, dass der Compiler eine ziemlich komplizierte Optimierung durchführt (Inline unique_ptr
-Destruktor in node
's, dann Schwanzrekursion verwenden), die viel schwieriger wird, wenn ich Folgendes tue (da der data
Destruktor die verschleiern würde next
, was es schwierig macht damit der Compiler die potenzielle Neuordnungs- und Tail-Call-Möglichkeit bemerkt):
struct node {
std::shared_ptr<node> next;
ComplicatedDestructorClass data;
}
Wenn es data
irgendwie einen Zeiger node
darauf gibt, kann es sogar unmöglich sein, den Schwanz zu rekursieren (obwohl wir uns natürlich bemühen sollten, solche Verstöße gegen die Kapselung zu vermeiden).
Wie soll man diese Liste dann im Allgemeinen sonst zerstören? Wir können die Liste nicht durchlaufen und den "aktuellen" Knoten löschen, da der gemeinsam genutzte Zeiger kein release
! Der einzige Weg ist mit einem benutzerdefinierten Deleter, der für mich wirklich stinkt.
quelle
gcc -O3
nicht möglich, eine Schwanzrekursion zu optimieren (in einem komplizierten Beispiel).Antworten:
Ja, dies wird irgendwann Ihren Stack sprengen, es sei denn, der Compiler wendet zufällig eine Tail-Call-Optimierung auf
node
den Destruktor undshared_ptr
den Destruktor an. Letzteres hängt stark von der Standardbibliotheksimplementierung ab. Die STL von Microsoft wird dies beispielsweise niemals tun, dashared_ptr
zuerst der Referenzzähler des Pointees (möglicherweise das Objekt zerstört) und dann der Referenzzähler des Steuerblocks (der schwache Referenzzähler) dekrementiert wird. Der innere Destruktor ist also kein Tail Call. Es ist auch ein virtueller Anruf , der es noch weniger wahrscheinlich macht, dass er optimiert wird.Typische Listen umgehen dieses Problem, indem nicht ein Knoten den nächsten besitzt, sondern ein Container, der alle Knoten besitzt, und eine Schleife verwendet, um alles im Destruktor zu löschen.
quelle
shared_ptr
. Ich kann die Zeiger nicht vollständig entfernen, da ich die Thread-Sicherheit brauchte.std::atomic_*
Überlastungen für sie, nein?std::atomic<node*>
kann und billiger.Späte Antwort, aber da niemand sie zur Verfügung gestellt hat ... Ich bin auf dasselbe Problem gestoßen und habe es mithilfe eines benutzerdefinierten Destruktors gelöst:
Wenn Sie wirklich eine Liste haben , dh jedem Knoten ein Knoten vorausgeht und höchstens einen Follower hat und Sie
list
ein Zeiger auf den ersten sindnode
, sollte das oben Genannte funktionieren.Wenn Sie eine unscharfe Struktur haben (z. B. ein azyklisches Diagramm), können Sie Folgendes verwenden:
Die Idee ist, dass wenn Sie:
Der alte gemeinsame Zeiger
next
wird zerstört (weil eruse_count
jetzt ist0
), und Sie zeigen auf Folgendes. Dies entspricht genau dem Standard-Destruktor, außer dass dies iterativ statt rekursiv erfolgt und somit ein Stapelüberlauf vermieden wird.quelle
next = std::move(next->next)
wobeinext->~node()
rekursiv aufgerufen wird.next->next
es (durch den Verschiebungszuweisungsoperator) ungültig wird, bevor der Wert, auf den gezeigtnext
wird, zerstört wird, wodurch die Rekursion "gestoppt" wird. Ich benutze diesen Code und diese Arbeit (getestet mitg++
,clang
undmsvc
), aber jetzt, wo Sie es sagen, ich bin nicht sicher , dass diese von der Norm definiert ist (die Tatsache , dass der verschobene Zeiger vor der Zerstörung des alten Objekts ungültig gemacht wird spitzer durch den Zielzeiger).operator=(std::shared_ptr&& r)
äquivalent zustd::shared_ptr(std::move(r)).swap(*this)
. Nach dem Standard ist der Verschiebungskonstruktor vonstd::shared_ptr(std::shared_ptr&& r)
maker
leer und daher vor dem Aufruf vonr
leer (r.get() == nullptr
)swap
. In meinem Fall ist dieses Mittelnext->next
leer, bevor das alte Objekt, auf das gezeigtnext
wird, zerstört wird (durch denswap
Aufruf).f
ist aktiviertnext
und nichtnext->next
. Da ernext->next
null ist, wird er sofort gestoppt .Um ehrlich zu sein, bin ich mit dem Algorithmus zur Freigabe intelligenter Zeiger eines C ++ - Compilers nicht vertraut, aber ich kann mir einen einfachen, nicht rekursiven Algorithmus vorstellen, der dies tut. Bedenken Sie:
Daher besteht keine Chance, dass der Stapel überläuft, und es ist viel einfacher, einen rekursiven Algorithmus zu optimieren.
Ich bin mir nicht sicher, ob dies in die Philosophie der "fast Null-Kosten-Smart-Pointer" passt.
Ich würde vermuten, dass das, was Sie beschrieben haben, keinen Stapelüberlauf verursachen würde, aber Sie könnten versuchen, ein cleveres Experiment zu erstellen, um mir das Gegenteil zu beweisen.
AKTUALISIEREN
Nun, das erweist sich als falsch, was ich zuvor geschrieben habe:
Dieses Programm baut ewig eine Kette von Knoten auf und dekonstruiert sie. Dies führt zu einem Stapelüberlauf.
quelle