A ist ein Objekt, das ein Signal liefert.
B und C sind Objekte die das Signal auswerten und daraus Ihren eigenen
Ausgang berechnen, der dann als Eingang für weitere Blöcke genutzt
werden kann.
Ich wollte das System so aufbauen, dass wenn der Ausgangsblock ("ganz
rechts") zerstört wird, dieser dann alle seine Abhängigkeiten zerstört.
Bei dem Bild oben tritt dann das Problem auf, dass A von zwei Blöcke
benötigt wird. Das heißt, dass wenn B zerstört wird A noch nicht
zerstört werden darf, weil es noch von C benötigt wird.
Ich hatte zunächst die Idee, dass über shared_ptr zu machen. Sprich B
und C führen einen shared_ptr auf A. Dazu müsste der von den beiden
zuerst erzeugte Block den shared_ptr auf A erzeugen und dann an den
anderen weitergeben, sobald dieser erzeugt wird. Das ist aber sehr
unschön, da B und C nichts voneinander wissen sollen.
Als Alternative könnte A einen Zähler über die Anzahl der verbundenen
Blöcke führen. Sobald dieser 0 wird, kann das Objekt zerstört werden.
Gefällt mir auch nicht wirklich, da A sich dann selbst zerstören muss
und das gewisse Risiken birgt.
Das Problem ist doch sicherlich nichts neues. Wie löst man das richtig?
Danish B. schrieb:> Das Problem ist doch sicherlich nichts neues. Wie löst man das richtig?
Bei richtigen Programmiersprachen (höhö) macht das der Garbage
Collector.
Du könntest ein 4. Objekt anlegen, dass für B und C die Instanz anlegt.
Das weiß dann ob es A anlegen muss oder die bereits bestehende Instanz
liefert.
Jan H. schrieb:> Bei richtigen Programmiersprachen (höhö) macht das der Garbage Collector
Ja, aber nicht zu einem definierten Zeitpunkt, also nicht unbedingt
sofort, wenn es keiner mehr braucht.
Danish B. schrieb:> Dazu müsste der von den beiden zuerst erzeugte Block den shared_ptr auf> A erzeugen und dann an den anderen weitergeben, sobald dieser erzeugt> wird.
Nicht unbedingt. B und C müssen ja irgendwo initialisiert werden. Und in
dieser Initialisierung verwendest du dann einen shared_ptr, den du den
beiden Instanzen jeweils übergibst.
Du musst ja so oder so eine Referenz auf A bereitstellen - die ersetzt
du dann durch den shared_ptr.
shared_ptr ist schon richtig, du solltest nur generell aufpassen keine
zyklischen Abhängigkeiten zu erzeugen und das SRP (single responsibility
principle) zu beachten.
Folgende Analogie. Stell dir vor A ist ein Arbeitgeber der B einen
Dienstwagen zur Verfügung stellt. Jetzt kommt ein zweiter Mitarbeiter
daher der ebenfalls einen Wagen benötigt. Anstatt diesen aber vom
Arbeitgeber zu bekommen erwartet der Mitarbeiter das Auto vom Kollegen
zu bekommen...
Hi,
entweder ich verstehe das Problem nicht, oder es sehr einfach.
A führt eine Liste von all seinen Nachfolgern und kennt die somit. Die
Nachfolger kennen ihrer Vorgänger. Wenn B zerstört werden muss, meldet
er das seinem Vorgänger. Dieser entfernt das Objekt aus seiner Liste.
Wenn die Liste leer ist, zerstört sich das Objekt selbst und meldet dies
wiederum seinem Vorgänger. Jedes Objekt hat also die Eigenschaften, dass
es sich selbst zerstören kann, dieses vorher seinem jeweiligen Vorgänger
meldet und selbst eine Liste seiner Nachfolger führt. Wenn nun ein
Objekt am Ende der "Kette" zerstört werden muss, wird eine Kaskade in
Gang gesetzt, die bis zum ersten Objekt laufen kann. Jedes Objekt
entscheidet dann selbst, ob es zerstört werden muss. Wie nun die
eigentliche Entfernung des Objektes aus dem Speicher passiert hängt von
der Programmiersprache ab.
Viele Grüße,
Servo
Noch ein Nachtrag:
Letztlich ist das Ganze ein Baum, und du musst prüfen, ob der Teil des
Baumes, an dem das zu zerstörende Objekt hängt, entartet ist, also nur
noch eine Liste darstellt. Diesen Ast sägst du dann ab.
Viele Grüße,
Servo
qwertzuiopü+ schrieb:> Danish B. schrieb:>> Dazu müsste der von den beiden zuerst erzeugte Block den shared_ptr auf>> A erzeugen und dann an den anderen weitergeben, sobald dieser erzeugt>> wird.>> Nicht unbedingt. B und C müssen ja irgendwo initialisiert werden. Und in> dieser Initialisierung verwendest du dann einen shared_ptr, den du den> beiden Instanzen jeweils übergibst.> Du musst ja so oder so eine Referenz auf A bereitstellen - die ersetzt> du dann durch den shared_ptr.
Das klingt gut. Das Objekt dass den Baum aufbaut übergibt dann einfach
einen shared_ptr an die beiden und gut ist.
Danke an alle!
Danish B. schrieb:> A ist ein Objekt, das ein Signal liefert.> B und C sind Objekte die das Signal auswerten und..
.. irgendwas machen. Also muß A einen Event liefern, der per Broadcast
(quasi CQCQ..) sein Signal in der Event-Queue herumposaunt. Das können
dann alle Objekte zur Kenntnis nehmen, die sich dafür interessieren.
Hat B oder C keine Lust mehr, sich zu beteiligen, dann beendet es eben
seine Mitgliedschaft in der Event-Verteilung. A geht sowas gar nichts
an.
W.S.
W.S. schrieb:> Hat B oder C keine Lust mehr, sich zu beteiligen, dann beendet es eben> seine Mitgliedschaft in der Event-Verteilung. A geht sowas gar nichts> an.
Darum ging es doch gar nicht. Wenn B und C zerstört werden, soll
automatisch auch alles zerstört werden, wovon die beiden abhängen. Das
hat mit irgendwelchen Event-Verteilungen doch nichts zu tun.
Im übrigen muss "Signal liefern" nicht heißen, dass es eine Eventloop
gibt. Das kann auch ein ganz ordinärer Funktionsaufruf sein.
Ich würde da auch sowas in der Art wie
Beitrag "Re: Ein Objekt gehört mehreren Objekten" vorschlagen.
M.K. B. schrieb:> Jan H. schrieb:>> Bei richtigen Programmiersprachen (höhö) macht das der Garbage Collector>> Ja, aber nicht zu einem definierten Zeitpunkt, also nicht unbedingt> sofort, wenn es keiner mehr braucht.
Das ist das Problem mit einem Garbage Collector. Man überlässt dem die
Verwaltung des Speichers, dafür muss man alles andere von Hand
verwalten. Und das lässt sich auch nicht ohne weiteres komplett
automatisieren, wie das in C++ mit RAII geht.
Abgesehen davon löst der GC auch nicht das Problem des TE, dass das
Objekt irgendwo mal angelegt werden und dann irgendwie zwischen B und C
ausgetauscht werden muss, ohne dass die von einander wissen.
Servo schrieb:> Letztlich ist das Ganze ein Baum
In gezeigten Fall ja.
Leider ist es auch nur dann so einfach. Sobald der Graph der
Abhängigkeiten komplexer wird, wird's wesentlich komplizierter bis
unlösbar.
Schöne Beispiele dafür sind:
Die Updatesysteme mittlerweile aller OS...
Aber auch komplexe Graphen in Media-Frameworks tendieren dazu, dieses
Problem aufzuzeigen.
Oder wild gewucherte Business-Logik in allen einschlägigen Umgebungen.
Das Problem bei all diesen Sachen ist, dass die Verfolgung der
Abhängigkeiten dann schnell mal zu einer Endlosschleife mutieren kann.
Und die Ursache dafür ist: agile Entwicklung. Jeder sieht nur sein
kleines Teil und die primitiven Strukturen darin, meist Listen oder
Bäume. Keiner aber überblickt mehr das Gesamtwerk und sieht, dass der
darin entstandene Graph schon lange kein Baum oder wenigstens ein Wald
mehr ist...
Das Schlimmste aber ist: dieses Problem kann u.U. sehr lange unter der
Oberfläche bleiben. D.h.: es ist schon lange potentiell da, bevor es das
erste Mal schädlich in Erscheinung tritt.
Wenn das aber passiert, dann schlägt die agile Entwicklung endgültig mit
gnadenloser Härte zu. D.h.: das Problem wird niemals wirklich gefixt,
weil niemand die Zeit/Kompetenz hat, es überhaupt zu erkennen,
geschweige denn ein verbindliches Regelwerk für die Graphenkonstruktion
einzuführen, die es beweisbar verhindern kann.
Denn das würde bedeuten, dass alles agil entstandene Gefrickel, was es
schon gibt, erneut angefasst werden müsste, um dieses Regelwerk auch
dort überall zu implementieren. Die Budgets für all diese Projekte sind
aber längst Geschichte...
So wird gefrickelt, gepatched, gework-arounded was das Zeug hält. Bis
zum bitteren Ende, wenn irgendwann garnix mehr gehen wird...
Danish B. schrieb:> Ich wollte das System so aufbauen, dass wenn der Ausgangsblock ("ganz> rechts") zerstört wird, dieser dann alle seine Abhängigkeiten zerstört.
Davon würde ich abraten. Solche Strukturen machen es praktisch
unmöglich, Beziehungen zu ändern oder Nodes (bei Dir "Blöcke")
auszutauschen.
Vorschlag:
Einen eigenen Container/Manager, der als Owner der Nodes (Blöcke)
fungiert. Die Nodes könnten per Factory-Methode vom Container/Manager
erzeugt werden oder Nodes werden außerhalb des Container/Manager erzeugt
(besser) und ihm übergeben. Dem Container/Manager gehören die Nodes.
Verbindungen könnten ebenfalls als eigenständige Objekte die dem
Container/Manager gehören modeliert. Sie könnten z.B. implizit vom
Container/Manager erzeugt werden, wenn er zwei Nodes verbinden soll.
Um Nodes zu zerstören, geht nur über den Container/Manager. Der stellt
auch sicher, dass die dazu gehörigen Verbindungen vernichtet werden.
So könnte das dann etwa aussehen:
1
// Universum für die Blöcke erzeugen
2
3
NodeGraph*universe=newNodeGraph(...);
4
5
6
// Universum mit Bloöcken füllen
7
8
Node*a=newNodeA(...);
9
Node*b=newNodeB(...);
10
Node*c=newNodeC(...);
11
12
universe->add(a);
13
universe->add(b);
14
universe->add(c);
15
16
universe->connect(a,b);
17
universe->connect(a,c);
18
19
20
// Später, wenn ein Block zerstört werden soll
21
22
Node*x=universe->findNode(...);
23
universe->destroy(x);
24
25
26
// Nicht verbunden Blöcke zerstören
27
28
universe->destroyUnconnectedNodes();
29
30
31
// Einen Block durch einen anderen ersetzen
32
33
Node*c=universe->findNode(...);
34
Node*c2=newNodeC(...);
35
36
universe->replaceNode(c,c2);
37
38
39
// Verbindungen ändern
40
41
Node*a=universe->findNode(...);
42
Node*b=universe->findNode(...);
43
Node*c=universe->findNode(...);
44
45
universe->disconnect(a,c);
46
universe->connect(b,c);
Das kann man natürlich beliebige mit Smart-Pointer verfeiner. Hier
möchte ich nur das Prinzip aufzeigen.
Der Punkt ist, der Container/Manager des Graphen sorgt dafür, dass die
Objekte zur rechten Zeit zerstört werden und kontrolliert auch die
Benutzung. Er kann sehr leicht sicherstellen, dass z.B. nur Nodes im
selben Universum verbunden werden etc.
Wenn man den Lifecycle von Objekten dagegen mit der Struktur eines
Graphen vermischt, wird die Sache schnell sperrig. Egal ob beim
Erstellen oder Verändern des Graphen.
Das ist halt das "nervige" an C++ bzw. an Sprachen ohne
Garbage-Collector. Man muss sich selbst um diese Aufgabe kümmern. Kann
von Vorteil sein, ist aber auch ganz schön viel Arbeit...
Experte schrieb:> Vorschlag:>> Einen eigenen Container/Manager, der als Owner der Nodes (Blöcke)> fungiert.> So könnte das dann etwa aussehen:>>>
1
>
2
>// Universum für die Blöcke erzeugen
3
>
4
>NodeGraph*universe=newNodeGraph(...);
5
>
6
>
7
>// Universum mit Bloöcken füllen
8
>
9
>Node*a=newNodeA(...);
10
...
11
>
Hier hast Du schon das erst potentielle Memory-Leak. Sobald der zweite
Ausdruck eine Ausnahme wirft, hast Du keine Referenz mehr auf den ersten
Speicher.
Danish B. schrieb:> Ich hatte zunächst die Idee, dass über shared_ptr zu machen. Sprich B> und C führen einen shared_ptr auf A. Dazu müsste der von den beiden> zuerst erzeugte Block den shared_ptr auf A erzeugen und dann an den> anderen weitergeben, sobald dieser erzeugt wird. Das ist aber sehr> unschön, da B und C nichts voneinander wissen sollen.
Ich glaube, Du hast std::shared_ptr<> evtl. nicht richtig verstanden.
Das wäre meiner Meinung nach genau die Lösung zu Deinen Anforderungen:
1
classA;
2
3
classB{
4
public:
5
explicitB(conststd::shared_ptr<A>&a)
6
:a_(a)
7
{
8
}
9
10
private:
11
std::shared_ptr<A>a_;
12
};
13
14
classC{
15
public:
16
explicitC(conststd::shared_ptr<A>&a)
17
:a_(a)
18
{
19
}
20
21
private:
22
std::shared_ptr<A>a_;
23
};
24
25
intmain()
26
{
27
autoa=std::make_shared<A>();
28
Bb(a);
29
Cc(a);
30
}
Da gibt es überhaupt keine Kopplung / Abhängigkeit zwischen B und C. Die
beiden Teilen sich ein A und sobald beide Referenzen auf das eine A
verschwinden, verschwindet auch das eine A. (Das ist genau die Aufgabe
von std::shared_ptr<>).
Hä??
was soll jetzt die Bewertung mit -1?
Beide mit -1 bewertete Ansätze sind korrekt und erfüllen genau die
Wünsche des TO oder seit Ihr alle so vernagelt, dass Ihr nur den
std::shared_ptr<> Pointer seht.
Kopfschüttel
Frank L. schrieb:> Hä??> was soll jetzt die Bewertung mit -1?>> Beide mit -1 bewertete Ansätze sind korrekt und erfüllen genau die> Wünsche des TO oder seit Ihr alle so vernagelt, dass Ihr nur den> std::shared_ptr<> Pointer seht.
Die erste Lösung enthält schon im Beispiel genügen Fehler, um zu
erkennen, dass sie nicht besonders robust ist. Spätestens beim Auffinden
des Knotens muss man die Type-Information, wieder selbst durch einen
Cast hinzufügen. Das ist auch fehleranfällig.
Der gesamte, benötigte Boilerplate-Code steht in überhaupt keinen
Verhältnis zur Komplexität der Aufgabe.
Klar, B und C könnten sicher Observer sein. Aber auch das löst nur die
Aufgabe, "Signale" zu verschicken, aber nicht, dass A gelöscht werden
soll, wenn es B und C nicht mehr gibt.
Torsten R. schrieb:> Klar, B und C könnten sicher Observer sein. Aber auch das löst nur die> Aufgabe, "Signale" zu verschicken, aber nicht, dass A gelöscht werden> soll, wenn es B und C nicht mehr gibt.
Hallo Thorsten,
doch, das löst es. Wenn das Array mit abhängigen Knoten leer ist, kann
der Inhaber ebenfalls gelöscht werden. D.h. Die Liste mit abhängigen
Knoten ist selber wieder ein Observer d.h. add und remove auf dieser
Liste werden im Objekt selber überwacht. Ist die Liste leer, kann das
Objekt sich löschen vorher hängt es sich aus einer Liste des eventuell
über geordneten Knoten aus. Damit würde im Zweifel der ganze Baum mit
einer Aktion gelöscht.
In einer Objektorientierten Umgebung kann man ein solches Verhalten über
eine Basisklasse vererben oder über Interface implementieren und in
allen Knoten vorhalten. Dabei ist es dann egal ob es ein Rootknoten, ein
beliebiger Ast oder ein Blatt ist.
Gruß
Frank
Frank L. schrieb:> doch, das löst es. Wenn das Array mit abhängigen Knoten leer ist, kann> der Inhaber ebenfalls gelöscht werden. D.h. Die Liste mit abhängigen> Knoten ist selber wieder ein Observer d.h. add und remove auf dieser> Liste werden im Objekt selber überwacht. Ist die Liste leer, kann das> Objekt sich löschen vorher hängt es sich aus einer Liste des eventuell> über geordneten Knoten aus.
Ja, Du kannst die Liste der Observer als "reference counter" verwenden.
Dann bekommst Du aber so Spezial-Fälle, wie das ein frisch konstruiertes
Objekt keine Observer hat, aber trotzdem noch lebt. Oder was passiert,
wenn A::attach() eine Ausnahme wirft? In A::detach() wird es ein `delete
this` geben müssen (nicht optimal).
B und C brauchen eh eine Referenz auf A, damit sie sich im d'tor aus der
Liste der Observer austragen können. Wenn diese Referenz als
shared_ptr<> implementiert ist, dann bekommst Du den Rest quasi
geschenkt.
Hallo Thorsten,
Danke für die ausführliche Antwort?
Wieder etwas gelernt! Wobei ich im C++ immer schon einen riesen Bogen
gemacht habe und nur Grundlagenkenntnisse habe.
Gruß
Frank
Frank L. schrieb:> Hallo Thorsten,> Danke für die ausführliche Antwort?> Wieder etwas gelernt! Wobei ich im C++ immer schon einen riesen Bogen> gemacht habe und nur Grundlagenkenntnisse habe.
Das ist das Problem: Dave Abrahams gehört oft nicht zum Kanon eines
einführenden Kurses.
Torsten R. schrieb:> Hier hast Du schon das erst potentielle Memory-Leak.
Gähn...
Was sind die paar Zeilen wohl? Ein Konzept oder eine fertige Lösung?
Wozu hab ich unter dem Beispiel wohl Smart-Pointer erwähnt?
M.K. B. schrieb:>> Garbage Collector>> Ja, aber nicht zu einem definierten Zeitpunkt, also nicht unbedingt> sofort, wenn es keiner mehr braucht.
Schau mal nach "Reference Counting" GC. Wird verwendet u.a. in
Perl 5, PHP, Python.
leo