Alltagsnotizen eines Softwareentwicklers Entkoppeln von Objekten durch Callbacks mit c++-interfaces oder boost.function und boost.bind Tags: c++, entkoppeln, objekt, oop, callback, boost.bind, boost.function, design Daniel Geschka 2010/11/09
1 Szenario: A besitzt B, kein Rückruf Eine Klasse A besitzt über Komposition eine Instanz einer Klasse B. A ruft Methoden von B auf. B weis nichts von A.A besitzt B (s. Beispiel) oder nutzt B via Assoziation. // Datei B.h: class B void g()... ; class A void f()b_.g(); B b_; ; // Datei main.cpp: A a; a.f(); Soweit alles ok. - 2 / 9 -
2 Szenario: B kennt A für Rückruf Erweiterung der Anforderung: B muss nun zusätzlich A benachrichtigen, bzw. eine oder mehrere Methoden von A aufrufen. Lösungsvorschlag: B kriegt einen Zeiger oder eine Referenz auf A: // Datei B.h: class B B(A& a):a_(a) void g() a_.f2(); A& a_; ; class A A():b_(*this) void f() b_.g(); void f2() B b_; ; // Datei main.cpp: A a; a.f(); - 3 / 9 -
Hier gibt es jetzt mehrere Probleme: Compiler-Fehler wegen Cross Includes: A.h inkludiert B.h. In B.h wird auf A verwiesen, was aber noch nicht bekannt ist (Compiler bricht mit Fehler ab). Lösung: Vorwärtsdeklaration von A in B.h. Was hier im Beispiel noch einfach geht, kann in der Praxis mit mehreren sich gegenseitig inkludierenden Headern / sich gegenseitig verwendenden Klassen sehr haarig werden. B, als die im Besitzverhältnis untergeordnete Klasse, weis viel mehr über A, als sie eigentlich wissen müsste, z.b. alle öffentlichen member von A. Änderungen an A führen auch zum Neu-Übersetzen von B. Im Beispiel noch überschaubar, kann das in echtem Code mit vielen Dateien sehr lange dauern, wenn über gegenseitiges Inkludieren und unnötige Komposition und Assoziation viele Klassen und Header oft dann auch ungewollt, bzw. eine Zeit lang unbemerkt indirekt miteinander verzahnt sind, womöglich auch noch über Modul-grenzen oder sogar Paket-Grenzen hinweg (Übersetzungszeiten von 10 Minuten+ fürs ganze Projekt). Klasse B kann nur von Klasse A verwendent werden. Oder man braucht einen A-Dummy nur, um von B eine Instanz anlegen zu können. Die Klassen A und B sind hier unnötig miteinander verzahnt. Vor allem B ist unnötig von A abhängig. 3 Szenario: Rückruf B nach A mit Interface Eigentlich würde es reichen, wenn A B kennt und B nur eine Funktion kennt, die sie auf, bzw. zurückrufen (Callback) kann. Eine mögliche Lösung ist hier, das B ein Callback-Interface definiert und vom Client (hier A) einen Zeiger oder eine Referenz übergeben kriegt, den B für den Callback benutzen kann: // Datei B.h: class B struct I virtual void f2()=0; ; B(I& i):i_(i) void g()... // Was arbeiten... i_.f2(); // Nachricht an i_ schicken/zurückrufen. I& i_; ; - 4 / 9 -
class A : public B::I A():b_(this) // b_ Zeiger auf selbst mitgeben (wir erfüllen B::I) virtual void f2()... // Der Callback, s. B::I void f()... b_.g(); // b_ benutzen B b_; ; // Datei main.cpp: A a; a.f(); Diese Lösung hat viele Vorteile: Der Cross-Include Compiler-Fehler kann so gar nicht auftauchen. B als untergeordente Klasse kennt A nicht. B weis nicht einmal, dass A hinter dem Interface steht. B muss insbesondere A.h nicht inkludieren. Ändert sich A, muss B nicht neu übersetzt werden. - 5 / 9 -
B kann nun auch von anderen Klassen genutzt werden. Das A von B::I erbt ist nicht schädlich, da es eine reine Schnittstellen-Vererbung ist. Insbesondere ändert diese nicht das Verhalten von A, wie es bei richtiger Vererbung von einer Basiklasse sein kann, wenn sich das Verhalten der Basiklasse ändert (Seiteneffekte!). 4 Szenario: Rückruf B nach A mit boost.function & boost.bind Eine andere elegante Möglichkeit besteht im Verwenden von Delegates. In c++ sind diese u.a. realisierbar mit Hilfe von boost::function (Funktionszeiger-Objekte) und boost::bind (festlegen von Methode, Instanz und Reihenfolge der Argumentübergabe beim verzögerten Funktionsaufruf). Der Effekt ist bei dieser Lösung der gleiche: B ist von A entkoppelt: // Datei B.h: #include <boost/function.hpp> class B // Übergabe eines Funktionszeigers im Konstruktor B(const boost::function<void()>& fn):fn_(fn) void g() fn_();// Aufruf mit operator(), hier ohne Rückgabewerte und Argumente boost::function<void()> fn_; ; #include <boost/bind.hpp> class A // Initialisieren von B b_ mit Übergabe des Funktionszeigers für den Callback A():b_(boost::bind(&A::f2,this)) virtual void f2()... void f() b_.g(); void f2()... // Die Callback-Funktion für B b_. B b_; ; // Datei main.cpp: A a; // Erzeugen von A a. a.f(); // a benutzt hier B b_, s. oben Alle Vorteile der Interface-Lösung gellten auch hier. Ein Interface existiert jedoch nicht. B kennt von seinem Client (hier der Klasse A) nur die Signatur der (Member-)Funktion, die B als Callback aufrufen soll. Mehr Entkoppelung geht wohl nicht. - 6 / 9 -
5 Include-Abhängigkeiten nach oben terminieren A.h ist von B.h abhängig. Ändert sich an B.h etwas, hat sich (include!) auch A.h geändert. Weitere Header, die A.h inkludiert haben, sind nun ebenfalls geändert Alle Dateien, die Header inkludieren, die ihrerseits A.h inkludiert haben sind nun auch geändert, usw.. Ändert sich also ein zentraler Header, wird daraus ruck zuck ein Neuübersetzen des ganzen Projektes. Auf geht s : #include <boost/bind.hpp> // Forwärtsdeklaration von B class B; class A A(); virtual ~A(); virtual void f2()... void f() b_->g(); void f2()... // Die Callback-Funktion für B b_. B* b_; ; // Datei A.cpp A::A() b_=new B(boost::bind(&A::f2,this)); - 7 / 9 -
A::~A() delete b_; B.h wird in A.h nun nicht mehr inkludiert. B wird über Vorwärtsdeklaration in A.h bekannt gemacht. Aus dem Member B b_ wurde ein Zeiger B* b_. Der Compiler benötigt zum Analysieren von A.h nicht die exakte Deklaration von B, wenn b_ ein Zeiger auf B ist. Es reicht ihm zu wissen, dass er für B* 4 Byte in A reservieren muss (für Zeiger unabhängig vom Typ immer gleich gross). In A.cpp wird nun B.h inkludiert. Der Konstruktor von A alloziert b_ auf dem Heap, ~A() dealloziert b_ wieder. Der Vorteil der Lösung: A.h ist nicht mehr von B.h abhängig. Ändert sich B.h muss nun nur noch A.cpp neu übersetzt werden. Die Kette der Include-Abhängigkeiten nach oben wurde durchbrochen 1 6 Weitere Beispiele zu boost.function & boost.bind Hier noch einige Beispiele für Callback-Methoden und freie Funktionen, die auch Argumente und Rückgabewerte haben: // Datei main.cpp #include <boost/function.hpp> #include <boost/bind.hpp> #include <string> // Für Methoden Callbacks struct X int f0()... int f1(double arg1)... std::string f2(double arg1, int arg2)... ; // Für Callback einer freien Funktion void foo(int arg1, double arg2, std::string arg3)... // Zum testen von member pointer functions: instanz von X x X x; // Beispiel: Rückgabewert, keine Argumente boost::function<int()> xf0(boost::bind(&x::f0,&x)); int result = xf0(); // return + 1 arg boost::function<int(double)> xf1(boost::bind(&x::f1,&x,_1)); int result = xf1(0.42); // return + 2 arg boost::function<std::string(double,int)> xf2(boost::bind(&x::f2,&x,_1,_2)); std::string result = xf2(0.43,4711); 1 In diesem Zusammenhang lohnt auch ein Blick auf die Klassen boost shared_ptr, bzw. scoped_ptr. - 8 / 9 -
// Beispiel für einen Callback/verzögerten Funktionsaufruf auf eine freie Funktion boost::function<void(int,double,std::string)> f(boost::bind(&foo,_1,_2,_3)); f(4711,0.42, hello world ); - 9 / 9 -