Objekt auf dem Heap oder Stack?
-
Was soll der Sinn des ganzen sein? Du erzeugst ein Objekt ganz am Anfang eines Programmes setzt dieses in einer anderen Klasse und am Ende des Programmes gibst du es wieder frei. Warum wird das SomeClassOfTheLibrary Objekt nicht gleich im Konstruktor von ASecondClassOfTheLibrary erzeugt und im Destruktor wieder gelöscht? Und dann gibst du im get immer nur nen const Pointer zurück.
Zum Zeitkritischen:
Naja, sagen wir es so, die Heap Version braucht nach gewissen Testmessungen, ca. 2 Sekunden, während der Stack es in den Millisekunden schafft.
Es kommt halt eben drauf an, was man macht.Was hast du denn da gemessen?
10000 malvoid foo() { Bla* bla = new Bla(); }
vs.
void foo() { Bla bla; }
Oder sowas?
-
@wasserzeitconverter,
Du könntest ja zum Beispiel auch am Anfang, set_class_pointer(NULL) setzen. Es kann auch sein, dass du das Objekt nur kurz brauchst. Es kann aber eben auch sein, dass du es wie oben, zu Beginn setzen musst und am Ende wieder entfernen. Ich kann ja nicht definieren, wo der User den Pointer setzen wird. Bzw. er muss ihn ja nicht mal setzen!Und zur Zeitmessung, ne, ich habe den ganzen schon vorhanden Code genommen und im Release Modus kompiliert. Dazu den DevPartner Profiler genommen (wow, hätte nicht gedacht, dass ich den so schnell wieder brauchen werde ^^). Es gibt noch andere Stellen die Zeit brauchen, aber durch die Heap->Stack umstellung, kann ich in gewissen Situationen, ca. 2 Sekunden einsparen.
Mit meiner neuen Idee für den Aufbau der Bibliothek, habe ich für alle Situationen, zumindest meiner Meinung nach, eine gute Lösung bereit.
Grüssli
-
Vielleicht verrätst du einfach mal was die Bibliothek machen soll und wozu du dieses komische Pointer setzen brauchst. Sehr wahrscheinlich gibts ne viel besser Lösung für das was du vor hast. Das man nur da Speicher auf dem Heap reserviert wo es sein muss und sonst immer den Stack verwendet sollte klar sein. Deine Verwendung von Heap/Stack schaut mir irgendwie sehr komisch aus.
-
wasserzeitconverter schrieb:
Vielleicht verrätst du einfach mal was die Bibliothek machen soll und wozu du dieses komische Pointer setzen brauchst. Sehr wahrscheinlich gibts ne viel besser Lösung für das was du vor hast. Das man nur da Speicher auf dem Heap reserviert wo es sein muss und sonst immer den Stack verwendet sollte klar sein. Deine Verwendung von Heap/Stack schaut mir irgendwie sehr komisch aus.
ich *vermute* mal, er will eine Art Objektbaum basteln, und das soll auf 2 Arten Möglich sein:
//1. Art wenn bekannt ist, wie der Baum aussehen soll Foo o1; Foo o2(&o1); Foo o3(&o2); //2. Art wenn es von irgendeiner Eingabe abhängt //alternativ auch mit irgendeiner Factory Foo* o1=new Foo(); Foo* o2=new Foo(o1); Foo* o3=new Foo(o2);
und Dravere sucht jetzt nen Weg, wie er das Speichermanagement bei der heap allocation geregelt bekommt. Da wir nicht wissen, inwiefern die lebensdauer determiniert ist, können wir ihm da aber nicht helfen.
könnte sein, dass sowas reicht:
Foo* o1=new Foo(); Foo* o2=new Foo(o1); Foo* o3=new Foo(o2); //dosomething... ... //end DoSomething delete o3; delete o2; delete o1;
für den Fall würde eine Factory reichen, die gleichzeitig besitzer der Objekte ist. wenn die Factory zerstört wird, nimmt sie ihre Objekte dann direkt mit.
Vielleicht reichts auch nicht, dann braucht man eine referenzzählung. Die würde ich aber niemals in das Objekt selber auslagern, sondern in form eines smart pointers kapseln.
@Dravere wegen deines tests: nutzt du einen Small Objekt allokator? Sonst ist dein benchmark nicht wirklich aussagekräftig, sondern eher auf dem niveau der üblichen java vs C++ Benchmarks.
-
Der Objektbaum, beschreibt es ziemlich gut. Grundsätzlich sind es einfach Metadaten, welche optionale Attribute haben, welche wiederrum Metadaten sein können (Metadaten über Metadaten?
).
Das mit der Lebensdauer ist ja genau mein Problem. Die Objekte, welche nur von der Bibliothek verwendet werden und ich die Kontrolle drüber habe, sind nicht das Problem. Da weiss ich genau, wie die Lebensdauer aussieht. Aber sobald jemand die Bibliothek benutzt, dann ist es möglich, dass er einzelne Metadaten selber setzen kann (logisch, nicht?). Und diese Lebensdauer kenne ich halt nicht und suche dafür trotzdem eine gute Lösung, welche so flexibel, angenehm und optimal für den Benutzer ist, wie nur möglich.
Also wenn die Lebensdauer länger ist, wird er wohl den Heap dafür benutzen und wenn es kurzfristig ist den Stack.
Wenn es längerfristig ist und er den Heap benutzt, er dann aber auch den Speicher selber wieder freigeben muss, dann bin ich mir sicher, führt das zuerst mal zu Memory Leaks. Und als nächtes zu einem Brummen, weil die Bibliothek nicht angenehm ist
Die temporäre Benutzung des Stacks, schliesst aber shared_ptr aus und auch die Möglichkeit den Pointer auf NULL zu setzen (Es soll ja optional sein), was bei shared_ptr nicht möglich ist.
Deshalb bin ich ja eben jetzt soweit, dass ich zwei Layer anbiete, einen für die schnelle Stack Benutzung und den anderen für die längere, dynamische Benutzung.
Das einzige Problem, welches somit bleibt, ist: "Wer gibt den vom User allokierten Speicher frei, bzw. wie?"
Ich bin halt eigentlich der Meinung, wenn man Speicher anlegt und diesen zur Verwaltung der Bibliothek übergibt, dann soll die den auch wieder freigeben. Und wenn so ein Referenzzähler direkt an das Objekt gekoppelt ist, dann kann man einfach selber noch ein increment_reference(); aufrufen, bevor man das Objekt der Bibliothek übergibt und man ist selber für die Freigabe des Speichers verantwortlich.
otze schrieb:
@Dravere wegen deines tests: nutzt du einen Small Objekt allokator? Sonst ist dein benchmark nicht wirklich aussagekräftig, sondern eher auf dem niveau der üblichen java vs C++ Benchmarks.
Da ich ehrlich gesagt nicht weiss, was ein Small Object Allocator ist, denke ich, hat mein Test mir falsche Werte geliefert
Grüssli
-
Dravere schrieb:
...Ich würde eher sagen, dass ist nicht benutzerfreundlich, sondern ein gesichertes Memory Leak...
Wenn der Nutzer da ein Memory leak einbaut, dann liegt das aber nicht an der Lib.
Bedenke mal Folgendes:
1.) Vgl. std::vector:vector<myClass*> v; v.push_back(new myClass); v.push_back(new myClass); //... //... irgendwo anders im Code: for(vector<myClass*>::iterator it = v.begin(); it != v.end(); ++it) { delete *it;
Ist ein ganz normales Vorgehen und verhält sich so, wie erwartet. Niemand hätte dem Standardkommittee vorgeworfen, hier benutzerunfreundlich zu sein und oder zu Memory Leaks zu zwingen.
2.) Ich würde Deine Lib genau so verwenden (erstmal ein new und später ein delete) und fände es äußerst unintuitiv, wenn die Lib sowas mit einem "double delete"-Absturz quittierte.
3.) Wie Du schon festgestellt hast, ist es gar nicht möglich, zwischen
... m_ASecondClassOfTheLibrary.set_class_pointer(new SomeClassOfTheLibrary); ...
und
... SomeClassOfTheLibrary a; m_ASecondClassOfTheLibrary.set_class_pointer(&a); ...
zu unterscheiden. Wenn mir Zweiteres verboten wird, fände ich DAS benutzerunfreundlich.
4.) Was soll bei Folgendem Szenario passieren ?
... ASecondClassOfTheLibrary a, b; SomeClassOfTheLibrary* p = new SomeClassOfTheLibrary; a.set_class_pointer(p); b.set_class_pointer(p); ...
Selbst wenn Du einen Mechanismus fändest, dass ASecondClassOfTheLibrary feststellt, dass das übergebene Objekt auf dem Heap liegt, würdest Du hier einen "double delete" erzeugen, der für den User unfindbar ist.
Am Besten fährst Du, wenn die Verantwortlichkeit ("Ownership") klar geregelt ist:
a) Wenn der Aufrufer für das Objekt verantwortlich ist und die Lib es nur verwendet => sie bekommt einen Zeiger und schert sich nicht darum, woher er kam und was mit ihm sonst noch so passiert.
b) Wenn die Lib für das Objekt zuständig ist, sollte sie es erzeugen, vernichten und möglichst keinen (non-const-)Pointer nach außen geben.
Alles Andere bringt Dich in Teufels Küche.BTW. Was spricht denn gegen Folgendes Modell:
class SomeClassOfTheLibrary{}; class ASecondClassOfTheLibrary { private: SomeClassOfTheLibrary* m_pClass; public: ASecondClassOfTheLibrary() : m_pClass(0) {} ~ASecondClassOfTheLibrary() { delete m_pClass; } template <typename T> set_class_pointer<T>() { m_pClass = new T; }; SomeClassOfTheLibrary const* get_class_pointer() { return m_pClass; }; };
Damit kannst Du Objekte für eine Vielzahl von Klassen anlegen (solange sie mit denselben Parametern konstruierbar sind - und ansonsten überlädst Du die Funktion).
Gruß,
Simon2.
-
Ein Small Object Allokator ist eine Art Speichermanager, der ähnlich wie Deque arbeitet. Er holt sich immer direkt große Datenblöcke und erstellt dann Objekte auf diesem, wenn ein neues Objekt angelegt werden soll. Wird sehr häufig in Verbindung mit Factories angewendet, und verrignert die Zahl der new Aufrufe drastisch.
Und diese Lebensdauer kenne ich halt nicht und suche dafür trotzdem eine gute Lösung, welche so flexibel, angenehm und optimal für den Benutzer ist, wie nur möglich.
Überlass es doch dem Benutzer die Lebenszeit zu bestimmen. Meistens wird er dies eh in einer Factory erledigen, und wird dabei schon selber genug sorge tragen, wann seine Objekte zerstört werden. Das ist jetzt auch nicht besonders "nutzerunfreundlich", denn der großteil der C++ Bilbiotheken funktioniert nach diesem Prinzip. Es wäre eher Benutzerunfreundlich, wenn sich deine Lib in die Speicherverwaltung einmischt.
Mal ein Beispiel für deine Referenzzählung: Der Benutzer hält selbst eine Referenz auf das Objekt. Nun bringt deine Referenzzählung nichtmehr viel, da das Objekt eh frühestens zerstört werden kann, wenn die externe Referenz freigegeben wurde, und damit liegt die bestimmung der Lebensdauer wieder Potentiell beim benutzer. Und auch hier gibt es neue Fehlerquellen: was ist, wenn er seine Referenz nichtmehr freigibt? Oder wenn er den Referenzzählungsmechanismus garnicht kennt? Dann kann das Objekt sogar zerstört werden, obwohl er es noch braucht, die folge ist der zugriff auf ein bereits zerstörtes Objekt, und damit undefiniertes verhalten(in 9 von 10 Fällen klappts...)
Und dieses Problem halte ich für viel Schlimmer als ein fehlendes delete. Ein fehlendes delete fällt den leuten meist selbst auf, spätestens profilerprogramme decken memoryleaks schnell auf. Du darfst nicht vergessen: new/delete sind für jeden C++ Programmierer ein bekanntes Konzept, referenzzählung nicht.
Referenzzählung bringt nur was, wenn du planst, die übergebenen Objekte an deine komplette objekthierarchie zu verteilen, sodass der Benutzer nichtmehr so ohne weiteres weis, ob er ein Objekt bereits löschen kann.
-
*genau das wie Simon schreiben gewollt*
-
Simon2 schrieb:
Wenn der Nutzer da ein Memory leak einbaut, dann liegt das aber nicht an der Lib.
Ich fühl mich halt verantwortlich ...
Simon2 schrieb:
... std::vector ...
Ich denke std::vector ist, wie ich schon mal gesagt habe, nicht vergleichbar. Ich benutze das Konstrukt auch oft so, wie du es zeigst.
Bei meiner Bibliothek sind es aber keine templates, bei mir sind es genau spezifizierte Klassen. Nur ein Objekt von einer bestimmten Klasse kann man setzen oder nicht und sonst nix anderes!Simon2 schrieb:
2.) Ich würde Deine Lib genau so verwenden (erstmal ein new und später ein delete) und fände es äußerst unintuitiv, wenn die Lib sowas mit einem "double delete"-Absturz quittierte.
Naja, man setzt ja allgemein auch voraus, dass der Benutzer der Bibliothek, die dazugehörige Dokumentation liest
Simon2 schrieb:
... m_ASecondClassOfTheLibrary.set_class_pointer(new SomeClassOfTheLibrary); ...
und
... SomeClassOfTheLibrary a; m_ASecondClassOfTheLibrary.set_class_pointer(&a); ...
zu unterscheiden. Wenn mir Zweiteres verboten wird, fände ich DAS benutzerunfreundlich.
Es würde ja nicht direkt verboten, sondern einen andere Layer dafür angeboten.
Simon2 schrieb:
4.) Was soll bei Folgendem Szenario passieren ?
... ASecondClassOfTheLibrary a, b; SomeClassOfTheLibrary* p = new SomeClassOfTheLibrary; a.set_class_pointer(p); b.set_class_pointer(p); ...
Dank der Referenzzählung ist das kein Problem. a erhöht den Zähler um eins und b erhöht ihn um eins. Wenn a vernichtet wird, wird der Zähler um eins vermindert und wenn b zerstört wird, geht der Zähler auf 0 und das Objekt, auf welches p zeigt, wird ebenfalls zerstört.
Simon2 schrieb:
a) Wenn der Aufrufer für das Objekt verantwortlich ist und die Lib es nur verwendet => sie bekommt einen Zeiger und schert sich nicht darum, woher er kam und was mit ihm sonst noch so passiert.
b) Wenn die Lib für das Objekt zuständig ist, sollte sie es erzeugen, vernichten und möglichst keinen (non-const-)Pointer nach außen geben.
Alles Andere bringt Dich in Teufels Küche.c) Beide sind zuständig ... was passiert nun?
Ja, sowas kann indirekt passieren. Du hast ein Konstrukt, wo bereits ein Pointer festgesetzt wurde, von der Bibliothek. Nun kommt der Benutzer der Bibliothek und will diesen Pointer durch seinen eigenen ersetzen. Wer gibt nun den Speicher vom Objekt frei, welches ursprünglich von der Bibliothek kam?
Oder das gleiche geht auch andersrum!
Es kann nicht festgestellt werden, wer den Zeiger gesetzt hat und somit ist auch nicht so klar, wer denn nun aufräumen muss. Da ist es deutlich einfacher, wenn in der Dokumentation steht, dass es einen Referenzzähler gibtSimon2 schrieb:
BTW. Was spricht denn gegen Folgendes Modell: ...
So ziemlich alles ^^
Ich will ja nur den Zeiger einer ganz bestimmten Klasse setzen. Da braucht es keine templates! Zudem müsste ich noch eine Möglichkeit einbauen, den pointer auf NULL zu setzen. Noch eine Methode zusätzlich, welche noch ein Reset durchführt.
Das Modell bietet eine enorme Anzahl an Möglichkeiten, welche ich alle gar nicht brauche und deshalb dann wieder beschränken müsste, was nur zusätzlicher Aufwand wäre.
Zudem wird da auch nur auf dem Heap allokiert
Und nicht zuletzt, kann man keine Objekte erzeugen, welche direkt über den Konstruktor die richtigen Werte zugewiesen bekommen.otze schrieb:
Ein Small Object Allokator ist eine Art Speichermanager, der ähnlich wie Deque arbeitet. Er holt sich immer direkt große Datenblöcke und erstellt dann Objekte auf diesem, wenn ein neues Objekt angelegt werden soll. Wird sehr häufig in Verbindung mit Factories angewendet, und verrignert die Zahl der new Aufrufe drastisch.
Sollte sowas nicht mein Compiler bei der Optimierung durchführen?
otze schrieb:
Es wäre eher Benutzerunfreundlich, wenn sich deine Lib in die Speicherverwaltung einmischt.
Sie mischt sich ja nur in den Bereich ein, welcher mit der Bibliothek zu tun hat. Und ist sogar da, noch sehr anpassbar, nämlich genau mit dem was du als nächstes sagst.
otze schrieb:
Mal ein Beispiel für deine Referenzzählung: Der Benutzer hält selbst eine Referenz auf das Objekt. Nun bringt deine Referenzzählung nichtmehr viel, da das Objekt eh frühestens zerstört werden kann, wenn die externe Referenz freigegeben wurde, und damit liegt die bestimmung der Lebensdauer wieder Potentiell beim benutzer.
Das ist doch genau das was ich auch zusätzlich möchte. So kann der Benutzer trotzdem noch selber über den Speicher bestimmen, falls er das möchte.
otze schrieb:
Oder wenn er den Referenzzählungsmechanismus garnicht kennt?
Ich setze voraus, dass jemand die Dokumentation liest. Sonst ist er selber Schuld
otze schrieb:
new/delete sind für jeden C++ Programmierer ein bekanntes Konzept, referenzzählung nicht.
Und alle probieren sich das new und delete angenehmer zu gestalten und kenen dadurch meistens auch die Referenzzählung :p
Grüssli
-
Dravere schrieb:
Simon2 schrieb:
Wenn der Nutzer da ein Memory leak einbaut, dann liegt das aber nicht an der Lib.
Ich fühl mich halt verantwortlich ...
Aus spass an der freude kannst du es ruhig machen - aber wenn deine Library sinnvoll eingesetzt werden soll - dann BITTE BITTE BITTE lass es.
der code wird nur umstaendlicher, langsamer, erzeugt abhaengigkeiten und verwirrt unnoetig.
Und alle probieren sich das new und delete angenehmer zu gestalten und kenen dadurch meistens auch die Referenzzählung :p
referenzzaehlung ist ein veraltetes konzept. manchmal ganz praktisch, aber einfach zu lahm um wirklich sinnvoll zu sein.
heutzutage verwendet man starke garbage collectoren - die sind einem ref counting um dimensionen ueberlegen und genau das ist der punkt. ref counting ist eine art GC mit kaum vorteilen eines GCs aber eine menge nachteile.
deshalb hat die c++ community gelernt, ref counting bzw. ring-smartpointer zu verwenden wenn es denn unbedingt sein muss (idr muss es nicht sein - ich habe zB noch nie in produktiven code so einen smartpointer verwendet -> scoped_ptr/auto_ptr reichen in 99% der faelle).
also bitte: wenn dein code produktiv eingesetzt werden soll, dann hoer auf die leute hier. ref counting war ein interessanter ansatz vor etlichen jahren. aber alleine wenn es um multithreading geht, ist ref counting einfach nicht mehr verwendbar (weil du dauernd locken musst - das kostet so sinnlos viel zeit...)
Und da du nicht weisst welche Threading Library verwendet wird, _kannst_ du ja garnicht mal locken. ergo ist dein Code nicht multithreading safe. und das in einer zeit wo jeder einen dual core rechner zuhause stehen hat...
-
Shade Of Mine schrieb:
heutzutage verwendet man starke garbage collectoren - die sind einem ref counting um dimensionen ueberlegen und genau das ist der punkt. ref counting ist eine art GC mit kaum vorteilen eines GCs aber eine menge nachteile.
Das leuchtet mir jetzt nicht wirklich ein, bisher empfand ich eher die GCs als extrem nachteilig, vor allem weil sie oft verzögert reagieren. Eine Referenzzählung dagegen befreit Speicher auf der Stelle.
Aber das hier auch noch zu diskutieren, führt wohl zu weit ^^Shade Of Mine schrieb:
Und da du nicht weisst welche Threading Library verwendet wird, _kannst_ du ja garnicht mal locken. ergo ist dein Code nicht multithreading safe. und das in einer zeit wo jeder einen dual core rechner zuhause stehen hat...
Das Argument sehe ich irgendwie nicht als sinnvoll. Eine nicht Multithreading sichere Bibliothek, ist ganz sicher schneller als eine sichere. Wenn jemand nun, nur einen Thread verwendet und die Bibliothek für mehrere ausgelegt ist, dann ist das reine Verschwendung. Eine nicht Multithreading Bibliothek kann jeder Zeit von ausserhalb abgesichert werden.
Aber es bringt alles nix, so habe ich immer noch keine Lösung, vor allem für den Punkt "c) Beide sind zuständig ..."
// In der Bibliothek class A {}; class B { private: A* m_pA; public: void set_a(A* pA) { m_pA = pA; }; A* get_a() { return m_pA; }; }; B* get_a_b() { static B b; b.set_a(new A); return &b; } /* * Die Funktion soll nur dafür stehen, dass * irgendwo in der Bibliothek ein B erstellt wird * und dabei ein A zugewiesen bekommt. */ // Ausserhalb der Bibliothek int main() { B* pB = get_a_b(); // Man bekommt irgendein B aus der Bibliothek // Man möchte das B nun anspassen: pB->set_a(new A); // Uuuups -> Memory Leak }
Grüssli
-
Shade Of Mine schrieb:
heutzutage verwendet man starke garbage collectoren - die sind einem ref counting um dimensionen ueberlegen und genau das ist der punkt. ref counting ist eine art GC mit kaum vorteilen eines GCs aber eine menge nachteile.
...
aber alleine wenn es um multithreading geht, ist ref counting einfach nicht mehr verwendbar (weil du dauernd locken musst - das kostet so sinnlos viel zeit...)Aber wie machen dann diese starken GCs? Die müssen doch auch wissen ob ein Objekt noch verwendet wird. Wie kann man das machen, ohne die Referenzen darauf zu zählen?
-
Dravere schrieb:
Shade Of Mine schrieb:
heutzutage verwendet man starke garbage collectoren - die sind einem ref counting um dimensionen ueberlegen und genau das ist der punkt. ref counting ist eine art GC mit kaum vorteilen eines GCs aber eine menge nachteile.
Das leuchtet mir jetzt nicht wirklich ein, bisher empfand ich eher die GCs als extrem nachteilig, vor allem weil sie oft verzögert reagieren. Eine Referenzzählung dagegen befreit Speicher auf der Stelle.
Aber das hier auch noch zu diskutieren, führt wohl zu weit ^^Es hat sich in den letzten 20 Jahren eine Menge getan
Heutzutage sind GCs enorm schnell - Java, C# und Co haben soviel schnellere Allokationen als wir C++ler. Man braucht schon starke Allokatoren um da mithalten zu koennen.GCs sind was performance betrifft mittlerweile meistens schneller als selber new/delete zu machen. Der Nachteil von GCs liegt darin, dass 1) die zerstoerung von Objekten irgendwann oder nie eintritt und dass es probleme mit real time anwendungen gibt.
Shade Of Mine schrieb:
Und da du nicht weisst welche Threading Library verwendet wird, _kannst_ du ja garnicht mal locken. ergo ist dein Code nicht multithreading safe. und das in einer zeit wo jeder einen dual core rechner zuhause stehen hat...
Das Argument sehe ich irgendwie nicht als sinnvoll. Eine nicht Multithreading sichere Bibliothek, ist ganz sicher schneller als eine sichere. Wenn jemand nun, nur einen Thread verwendet und die Bibliothek für mehrere ausgelegt ist, dann ist das reine Verschwendung. Eine nicht Multithreading Bibliothek kann jeder Zeit von ausserhalb abgesichert werden.
Das aber bedeutet, dass deine Library nicht Threadsafe ist. Ich muss selbstaendig ploetzlich jedes delete locken. das ist ein enormer aufwand und warum sollte es der anwender deiner library machen muessen?
nur weil dein code nicht thread safe ist, wird der client code ploetzlich enorm komplex.
Aber es bringt alles nix, so habe ich immer noch keine Lösung, vor allem für den Punkt "c) Beide sind zuständig ..."
// In der Bibliothek class A {}; class B { private: A* m_pA; public: void set_a(A* pA) { m_pA = pA; }; A* get_a() { return m_pA; }; }; B* get_a_b() { static B b; b.set_a(new A); return &b; } /* * Die Funktion soll nur dafür stehen, dass * irgendwo in der Bibliothek ein B erstellt wird * und dabei ein A zugewiesen bekommt. */ // Ausserhalb der Bibliothek int main() { B* pB = get_a_b(); // Man bekommt irgendein B aus der Bibliothek // Man möchte das B nun anspassen: pB->set_a(new A); // Uuuups -> Memory Leak }
Du versuchst Java in C++ zu programmieren. Das kann nicht klappen.
C++ ist anders als Java und deshalb loest man probleme anders.
Man verwendet zB RAII um keine new/deletes schreiben zu muessen. Das Problem mit dem Memory Leak hier hast du nur, weil dein Design Java-Like ist: speicher gehoert dem GC. In C++ gehoert Speicher aber nicht dem GC sondern dem der es erstellt hat. Uebersehen wir einmal das design problem dass du dadurch hast, dass set_a die implementierung von B veroeffentlicht, wem gehoert denn b?
ownership ist das essentielle in c++. man kann daran vorbeidesignen aber es wird ein patchwork werden.
nehmen wir als beispiel mal C++ streams. Ein stream hat einen streambuf den ich zur laufzeit veraendern kann und der virtuelle methoden anbietet. Wenn ich einem stream einen anderen streambuf gebe, dann loescht der stream diesen streambuf nie. Denn er gehoert ihm nicht. Ich kann den streambuf zB selbst erstellt haben, auf dem stack, auf dem heap oder aber von woanders bekommen haben - zB von einem anderen stream.
Der essentielle Punkt hier ist: "ich _kann_ ihn selbsterstellt haben, auf dem _heap_, _stack_ oder _sonstwo_". Das ist ebenfalls ein Konzept das Java nicht kennt. Ich kann mir allokatoren schreiben die mir bestimmte arten von speicher holen. Ich kann mir zB einen Garbage Collector zwischen schalten wenn ich denn will. komplett transparent.
ich kann schnelle small object allokatoren verwenden oder ich koennte den speicher am stack allokieren. vielleicht shared memory? wer weiss, wer weiss. ich habe diese moeglichkeiten - warum willst du sie mir wegnehmen?
der wirkliche vorteil von ref counting ist, dass man sich keine ownerships ueberlegen muss. dieser vorteil kostet aber enorm. zB ist hat ref counting enorme probleme in folgenden bereichen:
updates des ref counters - vorallem in multithreaded szenarien.
zyklische referenzen
Sehen wir uns einmal die updates des ref counters an. Wir starke techniken willst du hier verwenden? gehen wir mal von einer 08/15 implementierung aus (die untragbar waere, aber deutlich leichter zu verstehen).
Mir ist nicht ganz klar wie du dir das syntax maessig vorstellst, also weiche ich mal auf standard smartpointer syntax aus:
SmartPtr<Klasse> ptr=getKlasse(); someObj->set(ptr);
Dieser Code hat eine Menge Probleme. Es finden 2 Zuweisungen statt die den ref counter erhoehen. Jede diese Zuweisung muss gelockt werden, da getKlasse() das Objekt ja aus einem anderen Thread liefern koennte. Ich sehe hier zB nicht die Moeglichkeit die Locks im Clientcode einzubauen.
Wie will man die ZUweisung
ptr=getKlasse();
locken? Es ist nur im operator= moeglich. Wir muessen aber locken, da wir ein fetch, incremenet, write machen. also keine atomic operation haben. Durch defered increments kannst du das zwar reduzieren - aber der locking code muss da sein und da du nicht weisst wie man locken muss (du kennst ja die thread lib nicht) ist es nicht schoen moeglich. Du musst also eine schnittstelle anbieten um dem user es zu ermoeglich diverse locks einzubauen.Bei einem normalen ownership ansatz ist das alles kein problem: zugriffe auf den zeiger kann ich im client code problemlos locken - ich muss lediglich aufpassen dass der owner nicht vor mir stirbt.
Klasse* ptr = getKlasse(); LOCK(ptr); someObj->set(ptr);
denn zuweisungen sind ploetzlich nicht mehr thread unsicher
sehen wir uns jetzt aber das 2. Problem an:
Zyklische Referenzen. Bei boost hat man zB den ansatz gewaehlt weak_ptr anzubieten wenn man zyklische referenzen will - da der mehraufwand einfach recht gross ist. du kannst aber keine 2 modelle anbieten in deinem design und musst also entweder zyklische referenzen verbieten (was du aber nicht ueberpruefen kannst, du kannst nur warnungen in die doku schreiben) oder eine "weak_ptr" implementierung nehmen - die aber eben wieder mehr performance frisst.das resultat ist: dein code wird deutlich komplexer als wenn du den c++ weg gehen wuerdest und er wird deutlich langsamer. und nebenbei bemerkt baust du noch ein paar nette stolperfallen ein ueber die c++ programmierer fallen werden.
deshalb ein kleiner tip:
programmiere in C++ wie ein C++ programmierer, in Java wie ein Java programmierer und in python wie ein python programmierer.
-
&&&&& schrieb:
Shade Of Mine schrieb:
heutzutage verwendet man starke garbage collectoren - die sind einem ref counting um dimensionen ueberlegen und genau das ist der punkt. ref counting ist eine art GC mit kaum vorteilen eines GCs aber eine menge nachteile.
...
aber alleine wenn es um multithreading geht, ist ref counting einfach nicht mehr verwendbar (weil du dauernd locken musst - das kostet so sinnlos viel zeit...)Aber wie machen dann diese starken GCs? Die müssen doch auch wissen ob ein Objekt noch verwendet wird. Wie kann man das machen, ohne die Referenzen darauf zu zählen?
Ein GC laeuft ueber Collections. Eine Collection ist, wenn der GC die anwendung einfriert und dann alle referenzen durchzaehlt.
das ist in der theorie furchtbar langsam - in der praxis optimiert man da aber etwas.
Es gibt verschiedene strategien. Die Haupstrategie bei modernen GCs ist meistens Generationen basierend. Alle Objekte werden in Generationen eingeteilt: die generation zero (also die 1.) ist voll von objekten die gerade erst erstellt wurden. bei einer gen zero collection werden nur die objekte angesehen die seit der letzten collection erstellt wurden. dadurch kann man enorm schnell sein, da die meisten objekte nicht beachtet werden. alle objekte die diese collection ueberleben, wandern eine generation nach hinten. desto hoeher die generation, desto teurer die collection.
der wirkliche vorteil eines GCs liegt aber darin, dass er das "big picture" sieht. er lernt wie deine anwendung funktioniert und optimiert sich dahingehend. wenn er merkt du erstellst wenige objekte - laeuft er sehr selten. wenn er merkt du erstellst viele kurzlebige objekte, verstaerkt er sich auf die generation zero.
das schoene dabei ist, ein GC haelt die fragmentierung des speichers stabil und da er lernt, laeuft das programm eher schneller desto laenger es laeuft - waehrend du in nicht GC sprachen bei langen laufzeiten oefters mit speicher fragmentierung zu kaempfen hast.
und ein GC kann dir den speicher natuerlich schneller besorgen, da er genau weiss wo welcher speicher im cache liegt
-
Dravere schrieb:
...
Simon2 schrieb:
... std::vector ...
Ich denke std::vector ist, wie ich schon mal gesagt habe, nicht vergleichbar. ...
Nunja, Dein Argument ("Der Benutzer könnte vergessen, ein passendes delete zum new zu machen") treifft aber auf std::vector 100%ig zu ... damit müsste auch Deine Schlussfolgerung ("Benutzerunfreundlich und memory-leak-gefährlich") zutreffen.
Dravere schrieb:
...
Naja, man setzt ja allgemein auch voraus, dass der Benutzer der Bibliothek, die dazugehörige Dokumentation liest ;)...Mit DEM Argument kannst Du Dir das Ganze erst Recht sparen. Dann schreibst Du halt in die Doku "Am Ende delete machen !" und fertig.
Da Du aber (was löblich ist) Deine Schnittstelle intuitiv designen möchtest, solltest Du es ernst nehmen, wenn ich sage, dass Du hier an der Intuition von einem sehr großen Teil zukünftiger Benutzer vorbei designst.Dravere schrieb:
...
c) Beide sind zuständig ... was passiert nun?...Wir haben die Erkenntnis, dass es sich um schlechtes Design handelt. :p
Dravere schrieb:
...
...Du hast ein Konstrukt, wo bereits ein Pointer festgesetzt wurde, von der Bibliothek. Nun kommt der Benutzer der Bibliothek und will diesen Pointer durch seinen eigenen ersetzen. Wer gibt nun den Speicher vom Objekt frei, welches ursprünglich von der Bibliothek kam?...Natürlich der Benutzer, der ja sein eigenes Objekt erzeugt und "in die Lib gehängt" hat.
Dies ist gar kein Beispiel für "gemischte Verantwortung", sondern eines, wo man offensichtlich 2 Objekte mit unterschiedlicher Verantwortlichkeit hat.
(Die Lib könnte ihr Objekt zerstören, wenn es durch eines vom Benutzer ersetzt werden soll).Dravere schrieb:
...
Simon2 schrieb:
BTW. Was spricht denn gegen Folgendes Modell: ...
Ich will ja nur den Zeiger einer ganz bestimmten Klasse setzen. Da braucht es keine templates! ...
Umso besser ! Dann mach es doch ohne template.
Dravere schrieb:
...
Zudem müsste ich noch eine Möglichkeit einbauen, den pointer auf NULL zu setzen. Noch eine Methode zusätzlich, welche noch ein Reset durchführt....Hmmm, das glaube ich nicht. Dein Objekt braucht nur EINE Methode, die das tut, was es fachlich bedeutet, wenn der Zeiger "auf NULL gesetzt" wird.
Dravere schrieb:
...
Zudem wird da auch nur auf dem Heap allokiert
...Muss nicht. Es ging mir nur darum, die Verantwortung in die Lib zu verlegen. Sie kann (gerade DANN) problemlos auch Stackobjekte verwenden.
Dravere schrieb:
...
Und nicht zuletzt, kann man keine Objekte erzeugen, welche direkt über den Konstruktor die richtigen Werte zugewiesen bekommen....Natürlich kann man das. Dass meine "set-Funktion" keine Parameter entgegennimmt, ist natürlich nur ein Beispiel.
Natürlich kannst Du mit "Referenzzählung" eine Technik einsetzen, die dein momentanes technisches Problem ("double-delete") löst. Aber was da fachlich dahintersteckt, wenn "viele Köche im selben Brei rühren", wirst Du fachlich klären müssen.... und da kommt man schnell mal in Teufels Küche.
Mir scheint alllerdings, dass Du mit dieser DIskussion schon abgeschlossen und Dich auf smart_pointer/Referenzzählung eingeschossen hast. Dann wünsche ich Dir viel Erfolg (ganz un-ironisch !).Gruß,
Simon2.
-
Shade Of Mine schrieb:
...
Du versuchst Java in C++ zu programmieren. Das kann nicht klappen.
...Das bringt es schön auf den Punkt.
Gruß,
Simon2.
-
Shade of Mine schrieb:
deshalb ein kleiner tip:
programmiere in C++ wie ein C++ programmierer, in Java wie ein Java programmierer und in python wie ein python programmierer."Mir käme nicht in den Sinn, in C++ wie in Java zu programmieren. Nur fällt mir halt keine bessere Lösung ein, bzw. fiel mir ein und ich habe immer noch mühe ^^
Simon2 schrieb:
Mir scheint alllerdings, dass Du mit dieser DIskussion schon abgeschlossen und Dich auf smart_pointer/Referenzzählung eingeschossen hast.
Wenn dem so wäre, dann hätte ich schon lange gesagt, dass man die Diskussion schliessen kann und vor allem hätte ich inzwischen weiterprogrammiert, was aber nicht der Fall ist
Mein Problem ist derzeit, mir geht einfach nicht das Licht auf, wie ihr das Problem lösen würdet. Ihr sagt zwar "Design Fehler", aber eine wirkliche Lösung sehe ich nicht ^^
Daher schnell die Frage/das Problem, so kurz wie möglich zusammengefasst:
Wie kann ich bei einem Objekt der Klasse B, ein optionales Objekt der Klasse A setzen, so dass jeweils derjenige die Speicherverwaltung übernimmt, welcher das Objekt gesetzt hat?Mir ist einfach nicht klar, wie ich eine von der Bibliothek erstellten Baumstruktur mit optionalen "Elementen", dem Benutzer der Bibliothek übergeben kann, so dass die Biliothek sich dann immer noch um den womöglich allozierten Speichern kümmert, welcher durch Speicher, bzw. NULL-Pointern, vom Benutzer ersetzt wird, ohne dass im nachhinein dann, der Speicher des Benutzers auch berührt wird.
<- really confused
Grüssli
-
Dravere schrieb:
Mir ist einfach nicht klar, wie ich eine von der Bibliothek erstellten Baumstruktur mit optionalen "Elementen", dem Benutzer der Bibliothek übergeben kann, so dass die Biliothek sich dann immer noch um den womöglich allozierten Speichern kümmert, welcher durch Speicher, bzw. NULL-Pointern, vom Benutzer ersetzt wird, ohne dass im nachhinein dann, der Speicher des Benutzers auch berührt wird.
Beschreib einfach mal genauer was du willst. Die Baumstruktur koennte zB den Ownership selber uebernehmen. Du koenntest es ueber Templates loesen und somit garnicht erstmal eine dynamische Allokierung provozieren.
scoped_ptr/auto_ptr bieten sich auch immer an wenn es um owner ship transfer geht.
Uebrigens braucht man Zeiger/Referenzen nur dann wenn man laufzeit polymorphie verwendet - ansonsten kannst du immer kopien ziehen.
Schau dir auch mal Libraries an die aehnlich zu deiner sind. uU die C++ streams (auch wenn sie nicht die schoensten sind): du kannst bei einem stream dynamisch den buffer bestimmen.
-
Shade Of Mine schrieb:
Uebrigens braucht man Zeiger/Referenzen nur dann wenn man laufzeit polymorphie verwendet - ansonsten kannst du immer kopien ziehen.
Naja, bei Baumstrukturen oder verketteten Listen ja nicht (oder?:)).
Dravere schrieb:
Mir ist einfach nicht klar, wie ich eine von der Bibliothek erstellten Baumstruktur mit optionalen "Elementen", dem Benutzer der Bibliothek übergeben kann, so dass die Biliothek sich dann immer noch um den womöglich allozierten Speichern kümmert, welcher durch Speicher, bzw. NULL-Pointern, vom Benutzer ersetzt wird, ohne dass im nachhinein dann, der Speicher des Benutzers auch berührt wird.
Wie Shade schon meinte, eine gute Möglichkeit wäre es, dem Baum zu überlassen, wie er seinen Speicher verwaltet. Also im Prinzip so (um konkret zu sein):
class TreeNode { public: void setLeft( const string& v ) { if ( left != NULL ) // Kaputt machen left = // Allokieren left->val = v; } private: string val; TreeNode *left, *right; };
Das hat auch den großen Vorteil, dass du im Baum einen eigenen Speichermanager einbauen kannst. Wenn's z.B. ein spezieller Baum ist, der eigentlich immer ziemlich groß wird, kannst du im Vorhinein schon 1 MB (oder so) an Speicher holen und die Nodes/Knoten mit "placement new" auf diesem Speicher unterbringen; so musst du nicht so oft Speicher anfordern.
edit2: Und das läuft auch unter dem OOP-Prinzip des Information-versteckens: Von außen ist es dir völlig egal, wie der Baum den Speicher verwaltet, er macht's selbst und du brauchst dich nicht drum zu kümmern. Und wenn der Programmierer des Baums sich entscheidet, einen (anderen) Speichermanager einzubauen, bekommst du davon genau null mit
edit3: Shade, hättest du vielleicht Lust, einen Artikel über GCen für's Magazin zu schreiben? Das Thema ist auf jeden Fall hochinteressant!
-
Badestrand schrieb:
Shade Of Mine schrieb:
Uebrigens braucht man Zeiger/Referenzen nur dann wenn man laufzeit polymorphie verwendet - ansonsten kannst du immer kopien ziehen.
Naja, bei Baumstrukturen oder verketteten Listen ja nicht (oder?:)).
Warum nicht?
std::list<std::string> l; l.push_back(std::string("test"));
klappt wunderbar
edit3: Shade, hättest du vielleicht Lust, einen Artikel über GCen für's Magazin zu schreiben? Das Thema ist auf jeden Fall hochinteressant!
gib mir zeit und ich machs :p aber gregor und optimizer und die ganzen kollegen wissen da dimensionen mehr als ich... frag die mal.