SigSlot, eine Beziehung ohne Wissen.

!!!Achtung: Dieser Artikel ist veraltet. Ich lasse ihn dennoch für alle lesbar, da trotzdem noch viel (meiner Meinung nach) richtiges enthalten ist!!!

 

Eine Beziehung, ohne davon zu wissen?! Wie soll das gehen?

Ich weiß, es ist eine ziemlich seltsame Aussage, aber ich rede hier auch nicht von einer Zwischenmenschlichen Beziehung, sondern von einem bekannten Programmier-Pattern.

Was ist eigentlich ein Pattern?

Ein Pattern ist im Prinzip ein Lösungsvorschlag zu einem bekannten Problem. Die Pattern haben den Vorteil, dass sich schon viele kluge Menschen darüber Gedanken gemacht haben und dadurch eine Lösung zu Stande gekommen ist, die mit hoher Wahrscheinlichkeit für die entsprechenden Problemfälle gut zu benutzen ist. Das heißt natürlich nicht, dass man alle Probleme mit Hilfe dieser Patterns lösen muss. Man sollte immer abwägen, ob dem eigentlichen Problem damit geholfen ist oder ob man sich damit nur noch mehr Probleme auferlegt.

Aber genug von Patterns im Allgemeinen, kommen wir zu unserem spezifischen Problem.

Das observer bzw. listener Pattern:

Dieses Pattern beschreibt, wie 2 Objekte unterschiedlichen Typs miteinander kommunizieren können, ohne von einander zu wissen, wobei die Kommunikation eine Einbahnstraße darstellt.

Das heißt im Prinzip haben wir auf der einen Seite einen Sender und auf der anderen Seite mehrere Empfänger. Diese Empfänger lauschen jetzt darauf, bis der Sender sich meldet und ihnen ein Signal sendet, das sie verarbeiten können. Den Sender interessiert nicht, wer alles das Signal empfangen hat, oder was mit dem Signal genau passiert, er weiß lediglich, dass er es versendet hat. Damit ist seine Arbeit getan.

Dem Empfänger ist der letztendliche Sender auch egal, da er lediglich mitbekommt, dass ein Signal an kommt und nicht wer es sendet.

 

Verschiede Begrifflichkeiten:

Im Laufe dieses Posts werden einige Begrifflichkeiten im Bezug auf das Pattern auftauchen, die ich hier mal näher erklären möchte.

Signal: Im Prinzip ist ein Signal nichts anderes als man erwarten würde. Es signalisiert, dass ein Ereignis eingetreten ist und sendet das an seine Empfänger. Es kümmert sich nicht darum, warum es etwas schickt, noch an wen es etwas schickt. Alle Empfänger die auf dieses Signal „lauschen“ erhalten es auch.

Slot: Ein Slot ist eine Funktion, die durch ein Signal ausgelöst wird. Ein Slot weiß nicht, woher ein Signal kommt oder warum es gesendet wurde, er erhält lediglich die durch das Signal bereit gestellt Informationen.

Empfänger/Receiver: Ein Receiver ist ein Objekt, das ein oder mehrere Slots besitzt.

Verbindung/Connection: Die Beziehung von einem Signal zu einem Slot nennt man Connection.

Emit: Emit heißt so viel wie „aussenden“ und bezeichnet den Vorgang, wenn ein Signal ausgelöst wird.

Wie man sich nun denken kann, setzt sich SigSlot demzufolge aus den beiden Begriffen Signal und Slot zusammen 😉

 

Überblick über einige Implementierungen:

Für dieses System gibt es verschiedene Implementierungen, die einen besser, die anderen schlechter. Die Beste mir bekannte, dafür auch die aufwendigste, bietet das Qt Framework. Hierbei wurde das Pattern geschickt in die Sprache „verflochten“, sodass eine sehr intuitive und effektive Nutzung möglich ist. Allerdings ist das Signal/Slot System ein sehr, sehr großer Teil des Frameworks und hat viele Abhängigkeiten, sodass eine Nutzung in kleineren Projekten zu viel Overhead und Konfigurationsarbeit mit sich ziehen würde.

Eine andere mir bekannte Implementierung ist in der boost Library. Auch wenn ich mich bisher nicht sonderlich intensiv mit boost::signals bzw. boost::signals2 (die Thread sichere Variante) beschäftigt habe, ist das mit Sicherheit keine schlechte Alternative. Allerdings sind viele Leute der Ansicht, dass die boost::signals(2) Library nicht performant genug ist.

Nach ein wenig mehr Recherche habe ich die sigslot Implementierung gefunden. Auf den ersten Blick eine sehr robuste, allerdings auch veraltete Implementierung. Durch C++11 könnte man den Code wesentlich einfacher und sauberer gestalten. Der Hauptgrund, warum ich letztendlich gegen diese Implementierung bin ist, dass sie Rekursion nicht beachtet. Wenn man ein signal/Slot löscht, während man in der „emit“ Phase ist, läuft man unweigerlich in einen Error. Das ständig im Auge zu behalten, wann ich wo welche Objekte löschen darf, war mir ein Dorn im Auge; … ein ziemlich großer sogar.

Damit war letztendlich die Entscheidung gefallen, etwas eigenes zu entwickeln. Nach ein wenig herumprobieren und Stolpern über den ein oder anderen Denkfehler, habe ich schlussendlich ein Konzept entwickelt, was meinen Anforderungen stand hält und sich der Features des C++11 Standards bedient.

Ein kurzer Überblick der Probleme:

Es gibt, wie bereits gezeigt, verschiedene Möglichkeiten dieses Pattern zu implementieren. Um aber mal einen kleinen Überblick über die Probleme zu geben, die während der Entwicklung einer Implementierung entstehen können, versuche ich Schritt für Schritt aufbauende Beispiele zu liefern, wie man etwas angehen kann und was dabei die Nachteile sind.

Die wohl einfachste Möglichkeit um ein Signal zu versenden, ist eine Methode die eine Methode eines anderen Objekts aufruft.

class Label
{
public:
    void onButtonClicked()
    {
        // mach was
    }
};

class Button
{
    Label* m_pLabel;

public:
    Button(Label* pLabel)
        : m_pLabel(pLabel)
    {}

    void clicked()
    {
        m_pLabel->onButtonClicked();
    }
};

int main()
{
    Label label;
    Button button(&label);
    button.clicked();
}

Wie man sieht, ist das ganze sehr zweckmäßig über zwei einfache Methoden in den Klassen Label und Button geregelt. Label::onButtonClicked wird aufgerufen, wenn Button::clicked aufgerufen wird. Das Ganze bringt aber gewisse Unschönheiten mit sich. Will man denn jedes mal auch ein Label haben, wenn man einen Button erstellt? Was ist, wenn ich das nicht möchte?

Wie man sieht ist die Benutzung solch eines Designs zwar einfach aber auch sehr beschränkt. Ich muss mir für jeden Anwendungsfall meine eigene Button Klasse erstellen und alle Objekte, die ein Signal erhalten sollen, mindestens als Zeiger oder Referenz bekannt machen. Das bringt noch einen Haufen Probleme mit sich. Was ist z.B. wenn das Objekt, welches das Signal erhalten soll, gelöscht wird? Dann sind Aufräumarbeiten von Außen nötig. Das gilt es allerdings zu vermeiden, da solche Konstrukte äußerst fehleranfällig und schlecht zu warten sind.

Wir haben also mehrere große Nachteile:

  • Sender muss Empfänger explizit kennen
  • schlechte Erweiterbarkeit
  • kein automatisches Aufräumen bei Löschung von Objekten

Schauen wir uns die ersten 2 Punkte an, erkennen wir einen gewissen Zusammenhang, den wir uns im nächsten Abschnitt näher anschauen wollen.

Eine Möglichkeit das Ganze ein wenig generischer zu machen, wäre für unsere Buttonklasse einen std::vector mit Zeigern vom Typen Receiver (Empfänger) oder ähnlichem zu füllen. Wir könnten eine abstracte Methode „onButtonApply“ einführen, und somit unsere Empfänger dynamisch hinzufügen oder auch wieder löschen.

Das sähe dann in etwa so aus:

class Receiver
{
public:
    virtual void onButtonClicked() = 0;
};

class Label : public Receiver
{
public:
    void onButtonClicked()
    {
        // mach was
    }
};

class Button
{
    std::vector<Receiver*> m_pReceivers;

public:
    void addReceiver(Receiver* pReceiver)
    {
        m_pReceivers.push_back(pReceiver);
    }

    void clicked()
    {
        for (auto pRec : m_pReceivers)
            pRec->onButtonClicked();
    }
};

int main()
{
    Label label;
    Button button;
    button.addReceiver(&label);
    button.clicked();
}

Die Nachteile hier liegen auf der Hand:

  • Button ruft immer eine explizite Methode der Objekte auf
  • Dementsprechend für jedes Signal eine eventuell nur einmal genutzte Methode in der Interface Klasse
  • Kein automatisches Aufräumen

Ist zwar an sich keine allzu schlechte Sache, aber für eine generische Lösung noch viel zu beschränkt.

Überlegen wir also ein Stück weiter:

C++ bietet uns die Möglichkeit Funktionszeiger zu nutzen. Funktionszeiger sind im Prinzip Zeiger auf eine Stelle im Code. Jede Funktion hat ihre eigene Adresse, die man dadurch referenzieren kann. Schwieriger wird es bei Methoden (Klassenfunktionen). Diese funktionieren ein wenig anders, was den Aufwand für eine generische Lösung erhöht aber nicht unmöglich werden lässt.
Wir könnten uns dabei die STL zu Nutze machen und std::function und std::bind nutzen. Ein guter Weg, den wir im nächsten Schritt mal weiter verfolgen möchten.

#include <functional>
#include  <vector>
#include <iostream>

class Label
{
public:
     void onButtonClicked()
     {
          std::cout << "called!";
     }
};

class Button
{
     std::vector<std::function<void()>> m_pReceivers;

public:
     void addReceiver(std::function<void()> func)
     {
          m_pReceivers.push_back(func);
     }

     void clicked()
     {
          for (auto pFunc : m_pReceivers)
               pFunc();
     }
};

int main()
{
     Label label;
     Button button;
     button.addReceiver(std::bind(&Label::onButtonClicked, &label));
     button.clicked();
}

Super! Wir haben nun einen std::vector in dem wir verschiede Objekte beliebiger Typs mitsamt ihrer Methode aufnehmen können.

Allerdings bleiben noch 2 weitere Problem:

  • mühsam zu erweitern
  • kein automatisches Aufräumen

Um das Erweitern zu vereinfachen lagern wir den Signal Code aus Button in eine eigene Klasse Signal aus und erstellen uns einen public Member von Signal in Button. Diesen nennen wir „clicked“.

Das mag auf dem ersten Blick ein wenig verwirren, allerdings macht das bei näherem Hinsehen durchaus Sinn, so vorzugehen.

Dadurch betrachten wir nun die Signale nicht länger als reine Funktion, sondern als Objekte, was sie ja eigentlich auch sein sollen. Sie besitzen nun gewisse Automatismen, welche nötig sind um das Programm am Laufen zu halten und Fehlern vorzubeugen.

Diese Signal Objekte besitzen nun jeweils ihre eigene Liste der Empfänger und der Methoden, die sie aufrufen.

#include <functional>
#include  <vector>
#include <iostream>

class Label
{
public:
    void onButtonClicked()
    {
        std::cout << "clicked!";
    }

    void onButtonReleased()
    {
        std::cout << "released!";
    }
};

class Signal
{
    std::vector<std::function<void()>> m_pReceivers;

public:
    void addReceiver(std::function<void()> func)
    {
        m_pReceivers.push_back(func);
    }

    void emit()
    {
        for (auto pFunc : m_pReceivers)
            pFunc();
    }
};

class Button
{
public:
    // unsere beiden public Signal Member
    Signal clicked;
    Signal released;
};

int main()
{
    Label label;
    Button button;
    button.clicked.addReceiver(std::bind(&Label::onButtonClicked, &label));
    button.clicked.emit();
    button.released.addReceiver(std::bind(&Label::onButtonReleased, &label));
    button.released.emit();
}

Da wir nun Signale als Objekte betrachten und nicht als reine Funktionen, haben wir die Möglichkeit das Signal eigenständig arbeiten zu lassen. Sie halten ihre eigene Liste auf Empfänger und können die „Verbindung“ zu ihren Slots eigenständig trennen, wenn eines der beiden nicht mehr existiert.

Aber wie setzen wir das automatische disconnecten nun um?
Mein Lösung dazu ist, dass alle Objekte, die ein Signal empfangen sollen, von einer gemeinsamen Basisklasse erben müssen. Diese Basisklasse enthält den nötigen Code zum Lösen einer Verbindung. D.h. im Klartext: Das Empfänger Objekt sendet im Destruktor eine Anweisung an alle mit ihm verbundenen Signale, um die Verbindung zu trennen.
Das hat zwar zur Folge, dass ich nicht mehr alle beliebigen Objekte an mein Signal übergeben kann, sondern nur noch diese, die von der Receiver Klasse erben, aber das gibt mir auch entsprechende Sicherheit, dass ich mich um das Aufräumen nicht mehr kümmern muss.
Da ein Beispiel für die weitere Implementierung ein wenig zu weit gehen würde, poste ich hier ein paar Links, die zu meiner Implementierung führen.
Der Code ist dokumentiert und kann hier nachgelesen werden:

connection.h
receiver.h
receiver.cpp
signal.h

Ich würde mich über ein paar Kommentare freuen, gerne auch Kritik und Verbesserungsvorschläge 😉

mfg

2 thoughts on “SigSlot, eine Beziehung ohne Wissen.

  1. Hallöchen!
    Erstmal: Netter Beitrag. Als Einführung in das Design eines solchen Mechanismuses ist es definitiv lesenswert. Was ich da noch vermisse (könnte ja in kommenden Beiträgen kommen, oder ich tippe sowas bei mir, wenn ich Lust habe) sind die Antworten auf zwei wichtige Fragen:

    #1: Wie schmeiße ich Slots wieder aus dem Signal raus, wenn ich sie nicht mehr brauche?
    #2: Was passiert, wenn ein Slot eine Exception auslöst?

    Speziel Letzteres ist eine Frage, die mich von boost::signals2 abbrachte. Fliegt in Signals2 eine Exception, wird die Verarbeitung aller noch folgenden Slots unterbunden. Der fehlerhafte Slot bleibt zudem erhalten. In meiner Engine löse ich das so, dass jeder Slot ein „on_exception“-Attribut erhält, der bestimmt, ob eine Exception gelogt und ignoriert, den Slot rauswerfen, oder auch die Exception sammeln und später nested throwen soll.

    Wie auch immer, ein schöner Beitrag.
    Gruß und Segen,
    Evrey

    1. Hallo,
      erst einmal vielen Dank für dein Feedback.

      Deine Punkte sind verständlich, auf den 1. wollte ich eigentlich auch noch eingehen, allerdings habe ich die Kurve am Ende des Posts nicht richtig bekommen 😀
      Werde wohl noch einen 2. Post dazu machen, der ein bisschen weniger Tutorial Charakter besitzt und das Ganze noch ein wenig überarbeiten.

      Dein 2. Punkt leuchtet ein und ist auch sicherlich nicht unwichtig. Allerdings habe ich mir dafür selbst noch kein Konzept überlegt.
      Die exceptions der Connections catchen ist ja ziemlich trivial, allerdings die Behandlung derer nicht. Ich bin mir auch noch nicht ganz sicher, ob ein automatischer disconnect wirklich notwendig ist, wenn eine exception fliegt.
      Fände es cool, wenn du da deine Erfahrungen mit mir teilen könntest 😉

      mfg

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.