nullptr auf Stack und 0xCCCCCCCC
-
Hallo,
ok bezüglich des unique_ptr's war ich mir noch unsicher, wegen der Diskussion im oben verlinkten Thread (da gab es ja noch keinen so richtigen Konsens).
tissie schrieb:
Der Fehler liegt bei
children.back()->parent_ = this;, weil der node die Adresse ändert, wenn er kopiert wird.
Ok... aber ich kopiere doch nur den Pointer und nicht das node objekt selbst? Also das führt dann auch zu einer Adress-Änderung?
tissie schrieb:
- Mache dir die Besitz-Abhängigkeiten klar. Stelle sicher, dass node nur als unique_ptr erstellt werden kann (also mach den Konstruktor privat). Dann kann die Adresse nicht mehr geändert werden.
Äh, aber wie soll ich das Objekt dann erstellen wenn der Konstruktor privat ist? Und parent_ ist doch gar kein unique_ptr sondern ein roher Pointer, das geht dann folglich auch nicht?
-
In Zeile 43 kopierst du den parent-Node und zerstörst das Original. Irgendwie habe ich dieses Szenario befürchtet, als du in deinem anderem Thread gefragt hast, was make_shared genau macht. Die vorsichtigen Antworten dort hast du als Freibrief genommen, wild und verständnislos mit Pointern rum zu spielen.
-
node<T> *parent_
und
std::vector<std::shared_ptr<node<T>>> children
passen soweiso nicht zusammen, da die offensichtliche Invariante
this->children[x]->parent == this
nicht gelten kann, sobald ein Node von mehreren Knoten bessesen wird. Eigentlich sollten ja alle shared_ptr auf daselbe Objekt äquivalent sein, aber offenbar sind dann manche gleicher als andere.
-
happystudent schrieb:
Hallo,
ok bezüglich des unique_ptr's war ich mir noch unsicher, wegen der Diskussion im oben verlinkten Thread (da gab es ja noch keinen so richtigen Konsens).
tissie schrieb:
Der Fehler liegt bei
children.back()->parent_ = this;, weil der node die Adresse ändert, wenn er kopiert wird.
Ok... aber ich kopiere doch nur den Pointer und nicht das node objekt selbst? Also das führt dann auch zu einer Adress-Änderung?
nodeverändert nicht seine Adresse, das ist etwas unglücklich formuliert. Was tissie wahrscheinlich meint, ist dass der Kindknoten, den du inmake_a_node()zum Elternknotennhinzufügst, mit der Anweisungreturn nin einen neuen Knoten (mit anderer Adresse) kopiert wird, und somit dessen parent-Pointer auf nicht mehr auf seinen tatsächlichen Elternknoten verweist sondern auf den mittlerweile nicht mehr existierenden (temporären) Elternknotenn(Stichw. Dangling Pointer).Mögliche Lösungen wären z.B. ein Copy-Konstruktor, der (rekursiv) die parent-Pointer aller Kindknoten anpasst, oder (meines erachtens sinnvoller) eine
make_a_node()-Funktion die einen (Smart-)Pointer zurückgibt, anstatt den gesamten Baum, der annhängt zu kopieren.happystudent schrieb:
tissie schrieb:
- Mache dir die Besitz-Abhängigkeiten klar. Stelle sicher, dass node nur als unique_ptr erstellt werden kann (also mach den Konstruktor privat). Dann kann die Adresse nicht mehr geändert werden.
Äh, aber wie soll ich das Objekt dann erstellen wenn der Konstruktor privat ist? Und parent_ ist doch gar kein unique_ptr sondern ein roher Pointer, das geht dann folglich auch nicht?
Vermutlich will tissie auf so etwas wie eine Factory-Methode hinaus. Wenn du z.B.
make_a_node()z.B. zu einer statischen (public-) Methode vonnodemachst (odermake_a_node()als friend deklarierst), kann diese immer noch Knoten erstellen, da sie als Methode vonnodeden privaten Konstruktor aufrufen darf. Das würde ich allerdings auf den ersten Blick nur dann als sinnvoll erachten, wenn du sicherstellen willst, dass neue Knoten nur mitmake_a_node()erzeugt werden können (z.B. wenn wie tissie erwähnt hat, Knoten nur als unique_ptr erstellt werden sollen).Was die Wahl der Smart-Pointer angeht ("Besitzverhältnisse"): Shared Pointer machen eigentlich erst dann Sinn, wenn meherere Objekte "Besitzer" eines anderen sein können (shared ownership). Ich weiss nicht genau, wie du deine Baumstruktur geplant hast, da dein
nodejedoch nur einen parent-Pointer hat, kann ein Knoten höchstens einen Elternknoten haben - oder anders: Es kann für jeden Knoten nur einen einzigen anderen Knoten geben, der auf diesen verweist. Es liegt also eigentlich kein "shared ownership" vor, weshalb es auch Unique-Pointer tun, die weniger Overhead haben. Als Grundstruktur empfiehlt sich also eher so etwas in diese Richtung:struct node { node* parent; vector<unique_ptr<node>> children; ... unique_ptr<node> make_a_node() { unique_ptr<node> n(new node()); n->add_child(1); return n; } private: node() {} };Mir fällt natürlich auch ein Anwendungsfall ein, bei dem die Shared Pointer trotzdem Sinn machen:
Du möchtest z.B. Unterbäume deines Baums in andere Datenstrukturen "einhängen" und die Lebenszeit dieser Datenstrukturen kann die des Baums überdauern. In diesem Fall lohnt es sich sogar eventuell, den parent-Pointer zu einem Weak-Pointer zu machen, damit sich z.B. erkennen lässt, wenn Elternknoten nicht mehr existieren.
Finnegan
-
Hallo,
Danke schonmal für die ausführliche Antwort, ich habs jetzt soweit mit unique_ptr hinbekommen und es funktioniert auch (endlich) wie es soll.
Eine Frage hätte ich allerdings noch. Ich hab jetzt den vorgeschlagenen Weg einer statischen, erzeugenden Member-Funktion gewählt, nach diesem Prinzip:
template <typename T> class node { // ... public: static std::unique_ptr<node<T>> make(T const &data) { return std::unique_ptr<node<T>>(new node<T>(data)); } // ... };Das kappt jetzt auch soweit. Allerdings hab ich versucht den Aufruf von new durch make_unique zu ersetzen, nämlich so:
return std::make_unique<node<T>>(data); // In Zeile 8 obenwas ja genauso funktionieren sollte, oder?
Auf jeden Fall geht das nicht, da ich eine Compile-Fehlermeldung erhalte: "cannot access private member declared in class 'nodestd::string'".
was wohl daran liegt dass der Konstruktor jetzt privat ist... Aber wie soll man das umgehen?
-
happystudent schrieb:
was ja genauso funktionieren sollte, oder?
Auf jeden Fall geht das nicht, da ich eine Compile-Fehlermeldung erhalte: "cannot access private member declared in class 'nodestd::string'".
was wohl daran liegt dass der Konstruktor jetzt privat ist... Aber wie soll man das umgehen?
Ja, das Problem habe ich auch schon gehabt. Es gibt dafür eine portable Lösung, die allerdings zu etwas umständlicherem Code führt. Ich fasse zuerst nochmal kurz die Motivation hinter dem aktuellen Design zusammen, damit du dir nochmal überlegen kannst, ob du
makeund/odermake_uniquewirklich benötigst:Warum privater Konstruktor?
Der Konstruktor ist privat um sicherzustellen, dass Instanzen von
nodeausschließlich durch einen Aufruf vonmakeerstellt werden können. Gründe hierfür können z.B. sein:
- Du möchtest sicherstellen, dassnode-Instanzen nur als unique/shared-Pointer erstellt werden können (ist bei dir möglicherweise der Fall).
- Das Erstellen einer neuen Instanz erfordert zusätzlichen Code (z.B. irgendeine Form von Buchführung), der nicht im Konstruktor ausgeführt werden kann/soll.
- Singleton-Entwurfsmuster, bei dem sichergestellt weren soll, dass immer nur eine Instanz der Klasse existiert.Warum
make_unique?Im Gegensatz zu
make_shared, bei dem der Kontrollblock mit dem Referenzzähler und das Objekt direkt nebeneinander im Speicher erzeugt werden (cache-freundlich), hatmake_uniquekeinen zu erwartenden Performancevorteil.
Allerdings hatmake_uniqueden Vorteil "exception-safe" zu sein, wenn innerhalb eines Ausdrucks an verschiedenen Stellen Exceptions geworfen werden.
Beispiel:funktion(unique_ptr<T>(new T()), funktionDieExceptionWerfenKann());Hier kann der Code in folgender Reihenfolge auswertet werden:
new T()funktionDieExceptionWerfenKann()unique_ptr<T>(T*)
Wenn nunfunktionDieExceptionWerfenKann()eine Exception wirft, dann wird der Speicher, der durchnew T()reserviert wurde nicht freigegeben, da der reservierte Speicher noch nicht demunique_ptrzugewiesen wurde (Speicherleck). Mitmake_uniquekann das nicht passieren.
Bei deinem derzeitigen Code kann das allerdings nicht auftreten, da hier ausschließlich der Konstruktor von
Twerfen könnte → du kannst vorerst aufmake_uniqueverzichten.Wenn du dennoch
make_uniqueverwenden möchtest, ist eine Möglichkeit, den Konstruktor zwar public zu machen, ihm aber ein Argument zu geben, dessen Typ wiederum einen privaten Konstruktor hat, der mithilfe einer friend-Deklaration vonmakeaufgerufen werden darf:template <typename T> class node { public: static std::unique_ptr<node> make(T const& data) { return std::make_unique<node>(data, construct_via_make()); } class construct_via_make { // Statische make-Methode ist "friend", darf also // den Konstruktor construct_via_make() aufrufen. friend std::unique_ptr<node> node::make(T const&); // Konstruktor ist private (default für "class"). construct_via_make() {} }; // Konstruktor ist zwar public, erfordert aber als // zweites Argument eine Instanz von construct_via_make, // die nur von make() erstellt werden darf (friend). node(T const& data, construct_via_make const&) {} };Das ist zwar ein wenig umständlich, aber dafür eine portable Lösung. Man könnte zwar auch naiv erst einmal versuchen
make_uniqueals friend vonnodezu deklarieren, es gibt allerdings keine Garantie dafür dasmake_uniqueauch tatsächlich diejenige Funktion ist, die den Konstruktor vonnodeaufruft (das hängt von der Implementation der Standardbibliothek ab und kann für jeden Compiler anders sein).Wegen der zusätzlich erzeugten temporären Instanz von `construct_via_make
, die inmake` erzeugt wird, würde ich mir übrigens performance-technisch keine Sorgen machen. Jeder Compiler, der etwas auf sich hält wird so etwas komplett rausoptimieren.Gruss,
Finnegan
-
Ok, alles klar. Mir war nicht bewusst dass
make_uniqueanders alsmake_sharedkeinen weiteren Performancevorteil mit sich bringt.Da es dann hier ja "egal" ist ob
make_uniqueoder direkt der Konstruktor verwendet wird, werd ich dann bei der Lösung mitnewbleiben. Danke auf jeden Fall für die Erläuterungen
-
Au man, so viel Trouble wegen einer kleinen Baumklasse, da sind ja rohe Pointer ein Segen gegen, aber hier werden ja immer die "einfachen" Smartpointer in den Himmel gehoben.
Wenn ich solche Threads lese(und davon gibt es viele) dann vergeht mir schon wieder die Lust am C++ lernen.
-
CppTrouble schrieb:
Au man, so viel Trouble wegen einer kleinen Baumklasse, da sind ja rohe Pointer ein Segen gegen, aber hier werden ja immer die "einfachen" Smartpointer in den Himmel gehoben.
Och ich denke, dass Leute die Probleme im Umgang mit Smart-Pointern haben, auch auf Probleme mit rohen Pointern stossen werden.
-
CppTrouble schrieb:
Au man, so viel Trouble wegen einer kleinen Baumklasse, da sind ja rohe Pointer ein Segen gegen, aber hier werden ja immer die "einfachen" Smartpointer in den Himmel gehoben.
Wenn ich solche Threads lese(und davon gibt es viele) dann vergeht mir schon wieder die Lust am C++ lernen.
Wie theta schon sagte, Probleme gibts auch bei rohen Pointer.
Der Unterschied ist jedoch, dass man bei rohen Pointer die Fehler meist einfach nicht sieht oder bemerkt. Denn ein vergessenes delete, zum Beispiel, wird von niemandem gemeldet und davon abhängig können ganz andere Probleme im kanal untergehen ohne sie jemals zu sehen.
-
CppTrouble schrieb:
Au man, so viel Trouble wegen einer kleinen Baumklasse, da sind ja rohe Pointer ein Segen gegen, aber hier werden ja immer die "einfachen" Smartpointer in den Himmel gehoben.
Wenn man nicht weiß, wie man mit einem Hammer umgehen kann und Nägel immer mit einem Stein in die Wand haut, der ist darin so gut, dass er keinen Sinn sieht, bessere Werkzeuge wie Hämmer (ist das der Plural?) zu verwenden.
Wenn man jedoch einmal den Umgang mit einem Hammer gelernt hat, ist man darin wesentlich schneller.Man muss natürlich lernen mit seinen Werkzeugen umzugehen, bevor man sie schnell einsetzen kann. Und wenn man es kann, sind Smartpointer deutlich einfacher.