ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4440681
Jetzt (Chat):
19 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-
ZFX - C++ TutorialsDruckversion

Typsichere Callbacks mit libsigc++
Das C++ Tutorials von Markus Ewald aka Cygon erklärt die Benutzung von Callbacks unter Verwendung von libsigc++.

© Copyright 2003 [whole tutorial] by Markus Ewald aka Cygon
Die Texte der Tutorials unterliegen dem Copyright und dürfen ohne schriftliche Genehmigung vom Author weder komplett noch auszugsweise vervielfältigt, auf einer anderen Homepage verwendet oder in sonst einer Form veröffentlicht werden. Die zur Verfügung gestellten Quelltexte hingegen stehen zur freien Weiterverwendung in Software Projekten zur Verfügung.
Die Quelltexte dieses Tutorials werden auf einer "as is" Basis bereit gestellt. Der Autor übernimmt keinerlei Garantie für die Lauffähigkeit der Quelltexte. Für eventuelle Schäden die sich aus der Anwendung der Quelltexte ergeben wird keinerlei Haftung übernommen.


Typsichere Callbacks mit libsigc++

Weil so selten genutzt, möchte ich mit diesem Tutorial mal auf die Vorzüge der libsigc++ hinweisen. Die libsigc++ ermöglicht es, callback-Aufrufe nicht nur an herkömmliche Funktionen, sondern auch an Methoden von Klassen durchzuführen.

Um sogleich zur Tat schreiten zu können habe ich hier schonmal einen kompilierfertigen Workspace für MSVC6 bereitgestellt: sigc_demo.zip (Der Workspace befindet sich im Ordner Build\MSVC6\sigc_demo.dsw)

Die Homepage von libsigc++ ist zu finden unter http://libsigc.sourceforge.net. Versionen ab 1.2.0 funktionieren leider nicht mehr mit MSVC6 oder MSVC7 - 1.0.4 ist jedoch "stable", d.h. 1.2.0 hat neue Features, ist aber kein BugFix zu 1.0.4!

 

Kommen wir zum Inhalt des Tutorials:

 

Was sind Callbacks ?

Callback bedeutet soviel wie "Rückruf". Genau das passiert auch beim klassischen Callback: Der Programmierer übergibt die Adresse einer Funktion an eine andere Funktion, welche dann die übergebene Funktionsadresse ihrerseits aufrufen kann:

// Typ für Callback-Funktion
typedef void fnCallback(int);

// Ruft übergebene Callback-Funktion auf
void DoCallback(fnCallback *pfnFunctionToCall, int nValue) {
  pfnFunctionToCall(nValue);
}

In obigem Beispiel könnte der Programmierer der Funktion DoCallback() jede andere Funktion übergeben, die einen int-Parameter erwartet und einen void-Rückgabewert besitzt.
Da sich die Funktionsadresse auch zwischenspeichern lässt, kann man diese Technik vielfältig einsetzen. Zum Beispiel für ein Callback, das regelmässig aufgerufen wird, um die Anwendung über den Fortschritt eines langwierigen Vorgangs zu informieren.

Ein grosses Manko ist aber, dass solche Funktionen global sein müssen, also keine Methoden von Klassen sein können. Das liegt daran, dass eine Klassenmethode zusätzlich den this-Zeiger auf die Instanz, für die sie aufgerufen wurde, benötigt. Alle Daten, mit denen so eine Callback-Funktion arbeitet, müssten folglich global sein.

Abhilfe schafft hier ein weiterer Parameter vom Typ void *, den der Benutzer selbst definieren kann. Jedoch erfordert dies einen cast nach void * und wieder zurück, womit wir beim Thema Typsicherheit angelangt wären. Wird aus versehen etwas anderes als der erwartete Typ übergeben, stürzt die Funktion hoffnungslos ab ohne dass beim Kompilieren auch nur eine Warnung erschienen wäre.

Folgen wir der Evolution zu Callback-Interfaces:

// Interface für Callback-Funktion
class ICallback {
  public:
    virtual ~ICallback() {}
    virtual void onCallback(int nValue) = 0;
};

// Führt einen Callback mit dem übergebenen Callback-Objekt durch
void DoCallback(ICallback *pCallback, int nValue) {
  pCallback->onCallback(nValue);
}

Hierbei leitet der Programmierer eine Klasse von der Schnittstelle ICallback ab und implementiert dort die Funktion onCallback() nach eigenen Vorstellungen. Diese abgeleitete Klasse kann einen Zeiger auf die zu manipulierende Struktur als Member besitzen, oder gleich die zu manipulierende Struktur selbst sein!

Das ist aber nicht selten ein wenig übertrieben, vor allem wenn man Callback-Interfaces schreibt, die (wie oben) nur einzelne Methoden besitzen.

Nachdem wir also Callbacks im C-Stil und mit OOP-Techniken kennen, kommen wir nun zur generischen Variante.

 

Einfache callbacks mit libsigc++

Callbacks tragen bei sigc++ den Namen Signals, der stammt von einer vergleichbaren Technik namens Signals & Slots, welche in der GUI-Bibliothek QT zum Einsatz kommt. Ein Signal verwaltet die angemeldeten Callback-Funktionen und führt die Callback-Aufrufe durch. Ein Slot ist eine Funktion, die als Callback an einem Signal angemeldet wird - das gilt auch für sigc++.

Gezipptes MSVC-Projekt heruntergeladen und geöffnet ?

Beginnen wir mit einer grundlegenden Neuerung: Der Auslöser eines Callbacks ist bei sigc++ ein separates Objekt. Dieses dient sozusagen als Verbindungspunkt, an dem sich die Callbacks anmelden können. Nehmen wir an, wir wollten eine Klasse Button schreiben, welche die Anwendung über ein Callback informiert, wenn der Button gedrückt wurde.

Fügt folgenden Code einfach oberhalb von main() im Beispielprojekt ein:

// Ein drückbarer Button mit Callback-Benachrichtigung
class Button {
  public:
    // Callback-Verbindungspunkt
    SigC::Signal0<void> OnClick;

    // So wird das Callback aufgerufen
    void Click() {
      OnClick();
    }
};

SigC::Signal0 steht für ein Signal-template mit 0 Parametern. Das void für den gewünschten Rückgabetyp müssen wir trotzdem noch angeben. Hätten wir einen int-Parameter, müssten wir schreiben SigC::Signal1<void, int> OnClick, etc.

Der anschliessende Aufruf des Callbacks könnte einfacher nicht sein: OnClick() wird so aufgerufen, als handele es sich um eine herkömmliche Funktion. Tatsächlich ist OnClick() eine Klasse mit überladenem Funktionsoperator, allgemein auch als Functor bezeichnet, was uns aber nicht weiter interessieren muss.

int main() {
  // TODO: Write your stuff here!
  Button MyButton;
  MyButton.Click();

  return 0;
}

Noch passiert nix beim Aufruf, da sich ja auch noch kein Callback beim OnClick()-Signal des Buttons angemeldet hat.

Aber das holen wir sofort nach. Um das Callback an sigc++ zu übergeben, müssen wir es in einen SigC::slot wrappen, was aber nicht weiter kompliziert ist::

void ClickCallback() {
  cout << "Ouch!" << endl;
}

int main() {
  // TODO: Write your stuff here!
  Button MyButton;
  
  MyButton.OnClick.connect(SigC::slot(ClickCallback));
  MyButton.Click();

  return 0;
}

Tadaa! Hier ist unser erstes callback mit sigc++. Das Programm sollte beim ausführen jetzt "Ouch!" auf dem Bildschirm ausgeben (nicht vergessen, die Button-Klasse ist immernoch darüber!)

Sigc++ unterstützt auch multicast-callbacks. Das bedeutet, dass man beliebig viele Callback-Funktionen mit einem signal verbinden kann, und beim Auslösen des Signals sämtliche Callbacks aufgerufen werden. Solche Callbacks werden häufig auch Event genannt, wobei dann der Auslöser zum Publisher wird, und die Callback-Empfänger zu Subscribern.

Das ist schonmal ein kleiner Vorteil gegenüber dem Callback im C-Stil, weitere folgen nun:

 

Callback an Klassenmethoden

Eine (nicht-statische) Klassenmethode als Callback aufzurufen liegt durchaus im Sprachumfang von C++. Jedoch erfordert dies die zusätzliche Übergabe des this-Zeigers der Klasse und muss vom Autor des Callback-Ausführers vorgesehen worden sein.

Sigc++ dagegen benötigt keine weiteren Anpassungen am Signal-Auslöser. Nun wird auch der Zweck des Wrappers SigC::slot offensichtlich:

// Klasse zur Behandlung von klicks auf einen Button
class ClickHandler :
  public SigC::Object {
  public:
    void Callback() {
      cout << "Ouch!" << endl;
    }
};

int main() {
  // TODO: Write your stuff here!
  Button       MyButton;
  ClickHandler MyClickHandler;
  
  MyButton.OnClick.connect(SigC::slot(MyClickHandler, ClickHandler::Callback));
  MyButton.Click();

  return 0;
}

Die einzige Änderung (neben der neu eingeführten Callback-Empfängerklasse ClickHandler) liegt in der Verbindung des Callbacks mit dem OnClick()-Signal. Anstelle des Funktionszeigers wird dem this-Pointer einer Instanz von ClickHandler übergeben, sowie die Funktionsadresse einer Methode dieser Klasse.

Eine Callback-Empfängerklasse muss, wie aus obigem Beispiel ersichtlich, von SigC::Object abgeleitet sein. Das ist notwendig, um die Callback-Verbindung bei der Zerstörung von ClickHandler automatisch wieder zu lösen.
Wäre doch unschön, wenn der ClickHandler bereits zerstört ist, aber der Button, sobald er angeklickt wird, einen Aufruf an die nun ungültige Speicheradresse durchführt. Vielleicht ist dort ja auch schon ein anderes Objekt, dessen Aufgabe das Formatieren von Festplatten ist... ;-)

Da SigC::slot sämtliche Arten von Callbacks auf einen gemeinsamen Typen generalisiert können wir natürlich auch Funktionen und Klassenmethoden gleichzeitig mit dem Signal verbinden. Betrachten wir dazu nochmal ein abschliessendes Beispiel, welches alle bisher aufgeführten Szenarien umschliesst:

// Klasse zur Behandlung von klicks auf einen Button
class ClickHandler :
  public SigC::Object {
  public:
    void Callback() {
      cout << "Ouch!" << endl;
    }
};

// Funktion zur Behandlung von klicks auf einen Button
void ClickCallback() {
  cout << "Itch!" << endl;
}

int main() {
  // TODO: Write your stuff here!
  Button       MyButton;
  ClickHandler MyClickHandler;
  
  MyButton.OnClick.connect(SigC::slot(MyClickHandler, ClickHandler::Callback));
  MyButton.OnClick.connect(SigC::slot(ClickCallback));
  MyButton.Click();

  return 0;
}

Ein multicast-callback an eine nicht-statische Klassenmethode und an eine Funktion. Es sollte beachtet werden, dass keine Garantien über die Reihenfolge, in der Aufrufe erfolgen, gemacht werden!

Was aber nun, wenn wir mehrere Buttons haben ?
Registriert man dieselbe Callback-Funktion für alle Buttons, so kann man nicht unterscheiden, von welchem Button der Aufruf kommt. Natürlich könnte das Callback einen Parameter erhalten, in dem der Button einen ID-Wert übergibt. Vorausgesetzt natürlich, dass nicht vergessen wird, jedem Button die korrekte ID zuzuweisen.

Im folgenden Kapitel finden wir eine bessere Technik:

 

Callback mit inkompatiblem Funktionstyp

Hier haben wir ein Szenario mit drei Buttons. Um ein bisschen Abwechslung zu bieten, und um auf die Besonderheiten einer Klasse, die sich selbst als Callback anmeldet, einzugehen, enthält dieses Beispiel nun eine Dialogklasse, die drei Button-Objekte verwaltet (der Code für die Button-Klasse ist immernoch darüber und identisch mit obigen Beispielen!)

// Ein Dialog mit drei Buttons
class ThreeButtonDialog :
  public SigC::Object {
  public:
    // Konstruktor verbindet die callbacks für die Buttons
    ThreeButtonDialog() {
      m_Button1.OnClick.connect(SigC::slot(*this, &ThreeButtonDialog::buttonClicked));
      m_Button2.OnClick.connect(SigC::slot(*this, &ThreeButtonDialog::buttonClicked));
      m_Button3.OnClick.connect(SigC::slot(*this, &ThreeButtonDialog::buttonClicked));
    }
	
    // Simuliert einen Klick auf jeden der drei Buttons
    void simulateClick() {
      m_Button1.Click();
      m_Button2.Click();
      m_Button3.Click();
    }

  private:
    // Callback, ausgelöst wenn ein Button angeklickt wird
    void buttonClicked() {
      cout << "Ouch!" << endl;
    }

    Button m_Button1, m_Button2, m_Button3;
};

int main() {
  // TODO: Write your stuff here!
  ThreeButtonDialog().simulateClick();

  return 0;
}

Das Beispiel sollte dreimal hintereinander Ouch! ausgeben. Zu beachten ist, dass innerhalb der Klasse die Methodenadresse ein & vorangestellt bekommt, sowie dass der this-Zeiger dereferenziert werden muss.

Der Dialog ist offensichtlich nicht in der Lage, zu unterscheiden, von welchem Button der Aufruf kommt. Gegen Ende des letzten Kapitels habe ich es abgelehnt, jedem Button eine eindeutige ID zuzuweisen, die dann der Callback-Methode übergeben wird. Warum ?

Nun, die Vergabe der IDs würde durch die ThreeButtonDialog-Klasse geschehen. Wenn eine Funktion ausserhalb der Klasse als Callback registriert würde, könnte sie mit den IDs nicht mehr viel anfangen. Also müssen entweder die Buttons abgefragt werden (was schon wieder Anforderungen an die Reihenfolge der Registrierung stellt), oder die IDs müssen global verfügbar sein (was dann obendrein maximal einen offenen ThreeButtonDialog erlaubt).

Die Rettung trägt den Namen SigC::bind. Damit kann man inkompatible Funktionen an die Callbacks registrieren, und z.B. die fehlenden Parameter selbst vorgeben. Wie uns das hilft ?

#include <iostream>
#include "SigC++/Signal_System.h"
#include "SigC++/Bind.h"

using namespace std;

// Ein einfacher Button, der über Clicks benachrichtigt
class Button {
  public:
    SigC::Signal0<void> OnClick;

    void Click() {
      OnClick();
    }
};

// Ein Dialog mit drei Buttons
class ThreeButtonDialog :
  public SigC::Object {
  public:
    // Konstruktor verbindet die callbacks für die Buttons
    ThreeButtonDialog() {
      m_Button1.OnClick.connect(SigC::bind(SigC::slot(*this, &ThreeButtonDialog::buttonClicked),
                                           B_1));
      m_Button2.OnClick.connect(SigC::bind(SigC::slot(*this, &ThreeButtonDialog::buttonClicked),
                                           B_2));
      m_Button3.OnClick.connect(SigC::bind(SigC::slot(*this, &ThreeButtonDialog::buttonClicked),
                                           B_3));
    }
	
    // Simuliert einen Klick auf jeden der drei Buttons
    void simulateClick() {
      m_Button1.Click();
      m_Button2.Click();
      m_Button3.Click();
    }

  private:
    // Private IDs für die Buttons im Dialog
    enum BUTTON {
      B_1,
      B_2,
      B_3,
    };

    // Callback, ausgelöst wenn ein Button angeklickt wird
    void buttonClicked(BUTTON eButton) {
      switch(eButton) {
        case B_1: cout << "Ouch 1!" << endl; break;
        case B_2: cout << "Ouch 2!" << endl; break;
        case B_3: cout << "Ouch 3!" << endl; break;
      }
    }

    Button m_Button1, m_Button2, m_Button3;
};

int main() {
  // TODO: Write your stuff here!
  ThreeButtonDialog().simulateClick();

  return 0;
}

(Dies ist noch einmal der Quellcode in seiner Gesamtheit, da nun zusätzlich der Header "SigC++/Bind.h" notwendig ist).

Die Callback-Methode ThreeButtonDialog::buttonClicked() besitzt nun einen Parameter für die Button-ID, aber der Button-Event liefert garkeine solche. Betrachtet man den ThreeButtonDialog-Konstruktor etwas genauer, fällt sofort auf, dass hier die IDs mit im Spiel sind.

SigC::bind() ist in der Lage, den Callback-Aufruf auf die neue Methoden umzusetzen. Und zwar ohne üble Assembler-fummelei, sondern Compiler- und Platformunabhängig mithilfe von templates. Deshalb ist auch hier die Typsicherheit noch gewährleistet - ändert man z.B. den Parameter der buttonClicked()-Methode, meldet der Compiler sofort einen Fehler.

 

Wann sollten Callbacks eingesetzt werden ?

Die Erfahrung, wo sich Events/Callbacks auszahlen, und wo nicht, kann man nur selbst machen, da jeder Programmierer andere Codestrukturen entwirft.

Allgemein lässt sich aber sagen, dass man Events/Callbacks unter C++ am besten als "Benachrichtigung" versteht.

  • Die Quicksort-Funktion unter C nutzt z.B. einen Callback zur Übergabe einer Vergleichsfunktion - unter C++ ist das nicht mehr angebracht, dort gibt es für diese Zwecke überladene Operatoren und Templates.
  • Ein Callback ist auch nicht gedacht, um damit Polymorphie zu ersetzen, Beispielsweise ein RenderScene-Signal, an das sich der jeweils aktive Renderer einklinkt wäre ab dem Moment Schwachsinn, an dem der Renderer mehr als nur eine RenderScene()-Methode besitzt (und das tut er ganz sicher).
  • Ein gutes Beispiel für ein Einsatzgebiet wäre die in diesem Tutorial gezeigte Benachrichtigung von GUI-Elementen (einfacher, sicherer und schneller als eine WindowProc() wie bei Win32).
  • Es wäre auch denkbar, beim IDirect3DDevice::Reset() die Texturen und VertexBuffer-Objekte mit einem Callback zu informieren, statt den Renderer eine Liste der aktiven Objekte führen zu lassen.

Wenn es jedoch um eine Reihe von zueinandergehörigen Methoden geht, ist es vielleicht auch angebracht, ein gemeinsames Callback-Interface zu entwerfen, a la

class RenderQueueListener {
  public:
    virtual ~RenderQueueListener()

    virtual void preRender(Renderer *pRenderer) {}
    virtual void postRender(Renderer *pRenderer) {}
};

(Kommt mir das nicht aus Ogre bekannt vor ?)

 

So, das wars.

Ich hoffe ich konnte das Programmieren mit Event/Callbacks wenigstens ein bisschen Schmackhaft machen. An vielen Stellen eröffnen sich damit elegante Lösungen, wo man nurnoch in einem schmutzigen Hack einen Ausweg gesehen hat.

Zu laienhaft ? Zu komplex ?
Kommentare und Kritik bitte an Markus_Ewald@spam.gmx.net (weg mit dem spam.)
Briefbomben bitte an webmaster@127.0.0.1.



Informationen: " ); ?> " ); ?> " ); ?> " ); ?>
WWW :$wwwtitel
Data:Projektdateien
Mail:$author ($email)
Nick:$nick