2012-04-03 6 views
0

Ich habe Callbacks verwendet, um die Kopplung zwischen einigen C++ - Klassen zu reduzieren. Um Begriffe zu definieren: Ich rufe die Klasse an, die die Rückrufe durchführt, und die Klasse, die den Rückruf erhält, den Angerufenen. In der Regel (aber nicht notwendigerweise) besitzt der Angerufene den Anrufer. Der Anrufer hat vom Design her keine Kenntnis vom Angerufenen.Wie kann ich garantieren, dass die Lebensdauer eines Objekts der Dauer einer Mitgliedsfunktion entspricht?

Ich stoße auf ein Problem in Bezug auf die Lebensdauer des Caller-Objekts: Es gibt keine Garantie, dass es nach jedem beliebigen Callback noch am Leben ist. Nehmen Sie dieses einfache Beispiel:

void caller::f() 
{ 
    /* Some work */ 
    if (...) 
    { 
     /* [1] Execute callback */ 
     _callee->callback(this); 
    } 
    /* [2] Some more work */ 
} 

Sagen Sie, dass der Angerufene dynamisch den Anrufer zugewiesen hat, und hat für den Rückruf registriert speziell für einen bestimmten Zustand zu warten, auftreten. Wenn dies der Fall ist, löscht der Angerufene den Anrufer aus dem Rückruf bei [1]. Wenn das der Fall ist, kehrt die Steuerung zu caller :: f zurück, aber this wurde gelöscht, und jeder Code bei [2] wird höchstwahrscheinlich abstürzen.

Im allgemeinen Fall kann der Aufrufer nichts über den Angerufenen annehmen. Es weiß nicht, ob der Angerufene this besitzt, oder ob es this freigeben kann, also würde ich einige allgemeine Mittel benötigen, um die Freigabe für den Bereich der Aufruffunktion des Aufrufers zu verhindern.

Ich glaube eine mögliche Lösung dreht sich um boost::shared_ptrs und enable_shared_from_this, obwohl ich es nie benutzt habe. Da diese Callbacks sehr häufig auf mobilen Geräten mit begrenzter Verarbeitungsleistung ausgeführt werden (40+ Mal pro Sekunde), mache ich mir auch Sorgen über den Overhead des Erstellens und Entlassens dieser vielen shared_ptrs.

Delegierung ist ein ziemlich häufiges Muster in Objective-C. Ich bin weit weniger vertraut mit gängigen C++ - Entwurfsmustern. Gibt es eine schnelle und einfache Lösung für dieses Problem? Wenn nicht, wie würde dieses Design normalerweise in C++ erreicht?

+1

"Angenommen, der Angerufene hat den Anrufer dynamisch zugewiesen ... der Anrufer löscht den Angerufenen aus dem Rückruf ...". Das hört sich gar nicht nach einem guten Design an. – Chad

+0

@Chad: Warum ist das ein schlechtes Design? Wenn der Besitzer die Dienste des Arbeiters nach Erhalt eines Rückrufs nicht mehr benötigt, muss er ihn löschen. –

+0

@Chad: Tut mir leid, ich habe im fraglichen Satz "Caller" und "Callee" getauscht. Ja, die Art, wie ich es geschrieben habe, wäre schrecklich schlechtes Design. Ich hätte Begriffe mit mehr als einem einzelnen Buchstabenunterschied auswählen sollen ... –

Antwort

1

Gehen Sie voran und verwenden den gemeinsamen Zeiger, obwohl, wenn möglich, die Verwendung std::shared_ptr statt boost::shared_ptr. Es ist in der (aktuellen) Standardbibliothek, so dass keine unnötige Boost-Abhängigkeit hinzugefügt werden muss.Wenn Sie bereits Boost verwenden, ist das auch in Ordnung.

Sie haben nicht angegeben, über welche Art von Mobilgerät Sie sprechen, aber die Prozessoren in modernen Smartphones laufen mit Hunderten oder Tausenden von Megahertz, und sogar Low-Power-Telefone oft nur Java-Programme (mit Garbage Collection) fein. Geteilte Zeiger sind grundsätzlich Referenzzähler. Es ist keine ressourcenintensive Aktivität.

Wenn Ihr Gerät den Rückruf tatsächlich mehr als 40 Mal pro Sekunde ausführen kann, bezweifle ich, dass es Probleme mit freigegebenen Zeigern haben wird. Nicht vorzeitig für die Ausführungsgeschwindigkeit optimieren. Frühzeitig optimieren für Sicherheit und Gesundheit.

+0

Ich würde 'std :: shared_ptr' verwenden, aber aufgrund der Abhängigkeiten von statischen Bibliotheken, die mit der alten Standardbibliothek kompiliert wurden, kann ich nicht. Ich werde den shared_ptr-Ansatz verwenden, da er am elegantesten erscheint. Ich wollte nur sicherstellen, dass es keinen einfacheren Ansatz gab, bevor ich mich steigern konnte. –

1

Wenn der Aufrufer delete s Aufrufer, wird Destruktor des Aufrufers aufgerufen. Dort sollten Sie sicherstellen, dass f fertig ist.

Ich vermute, bin f ein Thread ist, so würde einfachste Lösung sein:

thread:

running = true; 
while (!must_exit) 
    /* do something */ 

destuctor:

thread->must_exit = true; 
while (thread->running) 
    sleep(a_little); 
/* continue with destruction */ 

Wenn f kein Gewinde ist, das gleiche Prinzip kann gelten, wo f sein Objekt (und dadurch seinen Destruktor) weiß, wann es läuft und wann nicht.


Wenn Sie nicht mit einem destructor Ansatz gehen wollen, können Sie immer noch diese Funktionalität durch eine Funktion implementieren, die die Angerufenen Anrufe, f sagen, nie wieder laufen und warten, bis sie stoppt. Dann fährt der Angerufene mit dem Löschen des Anrufers fort.

So etwas wie folgt aus:

void caller::f() 
{ 
    if (being_deleted) 
     return; 
    running = true; 
    /* Some work */ 
    if (...) 
    { 
     /* [1] Execute callback */ 
     _callee->callback(this); 
    } 
    /* [2] Some more work */ 
    running = false; 
} 

void caller::make_f_stop() 
{ 
    being_deleted = true; 
    while (running) 
     sleep(a_little); 
} 
+0

Ich könnte etwas speziellen Code im Destruktor hinzufügen, aber es würde auf einer Klasse-zu-Klasse-Basis variieren. Es scheint einfacher zu sein, zu vermeiden, dass der Destruktor überhaupt aufgerufen wird. Außerdem ist "f" in diesem Fall kein Thread, sondern nur eine eingekapselte Geschäftslogik. –

+0

@MattWilding Siehe mein Update – Shahbaz

+0

Entschuldigung, ich habe "Caller" und "callee" in einem kritischen Satz in der Frage getauscht. Der Angerufene (Eigentümer) löscht tatsächlich den Anrufer (Eigentümer). –

1

Die übliche Methode zur Umgehung von Code, die nicht mehr ausgeführt werden kann, ist das Auslösen einer Ausnahme. Dies sollte vom Angerufenen an dem Punkt erfolgen, an dem er normalerweise zum Anrufer zurückkehren würde, nachdem er den Anrufer gelöscht hat. Die Ausnahme würde am Ende der Funktion im Anrufercode abgefangen werden.

Ich kann nicht sagen, dass ich diese Lösung mag, aber ich denke, dass das von der ungewöhnlichen Situation des Angerufenen stammt, der den Anrufer besitzt.

Ich weiß nicht, wie intelligente Zeiger helfen würden, da niemand die zweite Kopie des Zeigers besitzen kann.

+0

Damit der intelligente Zeiger funktioniert, würde ich den Konstruktor privat machen und eine Factory-Funktion hinzufügen, die einen intelligenten Zeiger für die Erstellung zurückgibt. Der Anrufer besitzt den Angerufenen tatsächlich nicht. Es ist genau umgekehrt, obwohl ich die Worte an einer Stelle in der Frage getauscht habe. –

+0

@MattWilding, ich habe gerade den gleichen Fehler gemacht - ich wusste, dass der Angerufene den Anrufer besaß, ich habe ihn in meiner Antwort falsch geschrieben. Jetzt reparieren. Was den intelligenten Zeiger betrifft, besitzt der Besitzer des intelligenten Zeigers das Objekt, sofern nicht eine zweite Kopie vorhanden ist. Und mir ist immer noch nicht klar, wer die zweite Kopie haben wird, egal wie die erste Kopie erstellt wird. –

+0

Der zweite Zeiger würde nur intern in 'f' erstellt werden. Es ermöglicht dem Aufrufer im Wesentlichen, sich selbst teilweise zu übernehmen, bis der zweite gemeinsame Zeiger am Ende der Funktion den Gültigkeitsbereich verlässt. –

Verwandte Themen