Forum: PC-Programmierung Ein Objekt gehört mehreren Objekten


von Danish B. (danishbelal)


Lesenswert?

Ich habe folgenden Aufbau:
1
         +----- B ---
2
         |
3
A -------+
4
         |
5
         +----- C ---

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?

von Jan H. (j_hansen)


Lesenswert?

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.

von M.K. B. (mkbit)


Lesenswert?

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.

von qwertzuiopü+ (Gast)


Lesenswert?

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.

von Vincent H. (vinci)


Lesenswert?

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...

von Servo (Gast)


Lesenswert?

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

von Servo (Gast)


Lesenswert?

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

von Danish B. (danishbelal)


Lesenswert?

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!

von W.S. (Gast)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von c-hater (Gast)


Lesenswert?

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...

von Experte (Gast)


Lesenswert?

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 = new NodeGraph(...);
4
5
6
   // Universum mit Bloöcken füllen
7
8
   Node* a = new NodeA(...);
9
   Node* b = new NodeB(...);
10
   Node* c = new NodeC(...);
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 = new NodeC(...);
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...

von Frank L. (Firma: Flk Consulting UG) (flk)


Lesenswert?

Hallo Zusammen,

ich werfe mal das Observer Pattern in den Ring. Würde alle Anforderungen 
erfüllen und lässt sich in den meisten Programmiersprachen mit mehr oder 
weniger Aufwand realisieren.

https://en.wikipedia.org/wiki/Observer_pattern
https://www.philipphauer.de/study/se/design-pattern/observer.php

Gruß
Frank

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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 = new NodeGraph(...);
5
> 
6
> 
7
>    // Universum mit Bloöcken füllen
8
> 
9
>    Node* a = new NodeA(...);
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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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
class A;
2
3
class B {
4
public:
5
    explicit B( const std::shared_ptr< A >& a )
6
      : a_( a )
7
    {
8
    }
9
10
private:
11
    std::shared_ptr< A > a_;
12
};
13
14
class C {
15
public:
16
    explicit C( const std::shared_ptr< A >& a )
17
      : a_( a )
18
    {
19
    }
20
21
private:
22
    std::shared_ptr< A > a_;
23
};
24
25
int main()
26
{
27
    auto a = std::make_shared< A >();
28
    B b( a );
29
    C c( 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<>).

von Frank L. (Firma: Flk Consulting UG) (flk)


Lesenswert?

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

von Vincent H. (vinci)


Lesenswert?

Ja vor 20 Jahren hätte man solchen Code noch akzeptieren können...

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Frank L. (Firma: Flk Consulting UG) (flk)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Frank L. (Firma: Flk Consulting UG) (flk)


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Experte (Gast)


Lesenswert?

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?

von leo (Gast)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

leo schrieb:
> Schau mal nach "Reference Counting" GC. Wird verwendet u.a. in
> Perl 5, PHP, Python.

C++ (std::shared_ptr<>)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.