Object-Switching



  • Inhalt

    1. Einleitung
    2. Typschaltung mit Funktionspointer-Tabellen in C
    3. Implementierung des Objekt-Switching Patterns
    4. Verallgemeinerung des Objekt-Switching Patterns
    4.1 OnError-Policy
    4.2 Singleton-Muster
    4.3 Dummy Typ-Parameter
    4.4 Implementierung des allgemeinen Objekt-Switching Patterns
    5. Typschaltung globaler Funktionen
    6. Typschaltung polymorpher Elementfunktionen
    7. Erzeugung polymorpher Objekte
    8. Schlusswort
    9. Referenzen

    1. Einleitung

    Manchmal ist ein C- oder C++-Programmierer in der bedauernswerten Situation, sich mit ellenlangen switch-Anweisungen der Form

    //...
    switch(condition)
    {
      case condition_1:
        //....
        break;
      case condition_2:
        //
         break;
      //...
      case condition_n:
        //...
        break;
      default:
        //...
    }
    //...
    

    herumplagen zu müssen. Das wäre zunächst mal kein Problem, solange man selber nicht betroffen ist. Leider musste ich schon mehr als ein Review über derartig gestaltete Switch-Gräber ertragen. Das Einzige, was in solchen Fällen hilft, ist Ausdauer und eine volle Kanne mit schwarzem Kaffee der Marke Xtra-Strong. Ganz Hartgesottene schrecken auch nicht davor zurück, verschachtelte Switch-Anweisungen zu produzieren, die sich über hunderte von Zeilen erstrecken. Leider können solche Implementierungen weder vernünftig gewartet noch getestet werden. Wer zu derartigen Konstrukten neigt, sollte sich den folgenden Artikel unbedingt durchlesen.
    Dieser Artikel befasst sich mit einem Thema, das ich als Object-Switching bezeichnet habe. Object-Switching behandeln schlicht und ergreifend das immer wiederkehrende Problem, anhand eines Typ-Identifiziers eine Entscheidung treffen zu müssen. Object-Switching ist an das Design-Pattern der Object-Fabriken angelehnt, welches die Erzeugung polymorpher Objekte behandelt.

    //...
    Base * pObject;
    
    switch(condition)
    {  
      case condition_1:
        pObject = new Derived;
        break;
      case condition_2:
        pObject = new AnotherDerived;
        break;
      //...
    }
    //...
    

    Bem.: Dies ist in der Tat eine einfache Objekt-Fabrik

    Das nachfolgend vorgestellte Design-Pattern ist für polymorphe und nicht polymorphe Implementierungen gleichermaßen geeignet. Wie wir am Ende des Artikels sehen werden, kann das Muster auch als Objekt-Fabrik verwendet werden. Im Folgendem möchte ich mit euch ein Objekt-Switching-Pattern entwickeln, welches das zu Anfang dargestellte Problem löst.
    Es wird vorausgesetzt, dass alle aufrufbaren Entitäten (wie z. Bsp. Funktoren, Funktionsobjekte, etc.) über eine einheitliche Schnittstelle verfügen. Die Schnittstelle ist einheitlich, wenn der Typ des Return-Values und der Parameterliste für alle aufrufbaren Entitäten identisch ist.

    Selbstverständlich haben Switch-Anweisungen ihre Berechtigung, sonst wären sie kein Sprachelement von C/C++. Allerdings sollte man nur darauf zurückgreifen, wenn die Anzahl der zu treffenden Entscheidungen nicht überhand nimmt. Mir fallen zu Switch-Case-Anweisungen im Wesentlichen die folgenden Eigenschaften ein:

    • Abhängig von der Bedingung wird ein Zweig des Codes durchlaufen.
    • Es gibt keine Vorgaben, wie die Bedingungen in den jeweiligen Zweigen ausprogrammiert werden.
    • Alle Funktionen müssen innerhalb der Switch-Anweisung bekannt sein.
    • Es ist mit hohem Aufwand verbunden, den Ablauf des Programms zu ändern.
    • Hohe Performance, da Switch-Anweisungen direkt durchlaufen werden.
    • Für den Typ-Identifizierer muss ein integraler Datentyp gewählt werden.

    2. Typschaltung mit Funktionspointer-Tabellen in C

    In C werden solche statischen Konstrukte häufig durch Funktionspointer-Tabellen ersetzt. Der Code wird durch diese Maßnahme applizierbar, da für eine Änderung lediglich eine Funktion in der Tabelle geändert, eingetragen oder entfernt werden muss. Eine einfache Implementierung könnte z.B. wie folgt aussehen:

    typedef void (*fptr)(void); ///< Definition eines Funktionspointers
    typedef struct fptr_tab_s   ///< Definition einer Struktur um Bedingung und Funktion zu verknüpfen
    {
      int   condition;         
      fptr  func;
    } fptr_tab;
    
    #define CONDITION_1 2042
    #define CONDITION_2 1012
    //...
    #define CONDITION_n 5211
    
    fptr_tab array_fptr[] =
    {
      {CONDITION_1, function_1}, ///< der Bedingung Condition_1 wird die Funktion function_1 zugeordnet.
      {CONDITION_2, function_2}, 
      //....
      {CONDITION_n, function_n}
    };
    
    #define MAX_COUNT_ARRAY_FPTR sizeof(array_fptr)/sizeof(fptr_tab)
    
    int array_condition[] =
    {
      CONDITION_3,
      CONDITION_2,
      CONDITION_9,
      //...
      CONDITION_8,
      //...
    };
    
    #define MAX_COUNT_ARRAY_CONDTION sizeof(array_condition)/sizeof(int)
    
    //...
    
    for(k = 0; k != MAX_COUNT_ARRAY_CONDITION; ++k)
      for(j = 0; j != MAX_COUNT_ARRAY_FPTR; ++j)
        if(array_fptr[j].condition == array_condition[k])
          array_fptr[j].func();  //Funktion aufrufen
    
    //...
    

    Bem.: Die Tabelle array_condition[] simuliert Ereignisse, die z.B. zur Laufzeit angestoßen werden.

    Implementierungen, die Funktionspointer-Tabellen verwenden, sind wesentlich übersichtlicher, da Ablauf und Logik an zentraler Stelle in einer Tabelle beschrieben werden. Ich habe die Erfahrung gemacht, dass solche Tabellen auch von Nicht-Programmierern (z. B. Verfahrenstechnikern) gepflegt werden können.

    Die Eigenschaften von Implementierungen, die Funktionspointer-Tabellen verwenden, lassen sich wie folgt zusammenfassen:

    • Es wird ein Typ-Identifizierer benötigt. Im Idealfall kann der Index der Tabelle als Typ-Identifizierer gewählt werden (z.B. für eine State-Maschine).
    • Zu jeder Bedingung muss eine Funktion geschrieben (Forced by Design).
    • Alle Funktionen müssen der Tabelle bekannt sein (Compiler-Abhängigkeiten).
    • Abläufe können einfach geändert werden (z.B. Abändern der Einträge von fptr_tab array_fptr[] ).
    • Niedrigere Performance, da Typ-Identifizierer gesucht werden müssen (Optimierung z.B. durch binäres Suchen möglich). Diese Eigenschaft trifft natürlich nicht zu, wenn der Tabellenindex als Typ-Identifizierer verwendet wird. Außerdem muss ein zusätzlicher Funktionspointer dereferenziert werden. Der Performanceverlust durch die Dereferenzierung eines Funktionspointer kann normalerweise vernachlässigt werden.
    • Es kann ein beliebiger Typ-Identifizierer gewählt werden.

    3. Implementierung des Objekt-Switching Patterns

    Betrachen wir nun aber eine echte C++-Lösung, die zur Laufzeit den Typidentifizierer und die aufrufbare Entität speichert. Zur Speicherung der Daten bietet sich die Zuordnungsliste std::map aus der Standardbibliothek an. Der Container std::map gehört zu den assoziativen Containern, der seine Elemente nach einem frei wählbaren Schlüsselwert sortiert hält. Assoziative Container sind intern als binärer Baum organisiert, was ein schnelles Auffinden der Elemente anhand des Schlüssels ermöglicht. Die Datenelemente bestehen aus Schlüssel-/Datenpaaren vom Typ std::pair. Um Elemente in einen assoziativen Container einzufügen, muss ein Objekt vom Typ std::pair erzeugt werden.

    void insert_into_map(void)
    {
      typedef std::map<int,std::string> s_map;
      bool success;
      s_map string_map;
      success = string_map.insert(s_map::value_type(1,"Hallo")).second;
      if(success)
        success = string_map.insert(s_map::value_type(2,"OK")).second; //success = true
      success = string_map.insert(s_map::value_type(2,"Not OK")).second;   //success = false
    }
    

    Bem.: value_type ist eine Typedefinition des Containers std::map, mit dessen Hilfe der Paartyp erzeugt wird. Alternativ könnte auch std::make_pair verwendet werden.

    Bem.: Die Member-Funktion pair<iterator,bool> insert(const value_type& value) gibt ein Werte-Paar zurück, das einen Iterator auf das soeben eingefügte Element (first) und einen boolschen Wert (second) enthält. Der boolsche Wert gibt Auskunft darüber, ob die Einfügeoperation erfolgreich war.

    Um Elemente zu löschen, verwenden wir die Member-Funktion erase.

    //..
    typedef std::map<int,std::string> s_map;
    bool success(false);
    s_map string_map;
    int key(1);
    //...
    success = (string_map.erase(key) == 1);
    //...
    

    Bem.: Die Member-Funktion erase gibt die Anzahl der gelöschten Elemente zurück.

    Das Suchen von Elementen erfolgt mit der Member-Funktion find, die einen Iterator zurückgibt.

    //..
    typedef std::map<int,std::string> s_map;
    s_map string_map;
    //...
    
    const std::string & get_value(int key)
    {
      s_map::const_iterator s_iter = string_map.find(key);
      if(s_iter == string_map.end())
      {
        throw std::runtime_error("key not found");
      }
      return s_iter->second;
    }
    //...
    

    Bem.: Falls das gesuchte Element nicht gefunden werden kann, wird eine Exception vom Typ std::runtime_error ausgelöst.

    Mit diesem Wissen können wir eine Templateklasse schreiben, welches die Funktionalitäten von std::map auf die benötigten Elemente einschränkt.

    #include <map>
    
    template <typename Key, typename Data>
    class register_table
    {
    public:
      typedef Key key_type;
      typedef Data data_type;
      typedef std::map<key_type,data_type> table_type;
      typedef typename table_type::value_type value_type;
      typedef typename table_type::iterator   iterator;
      typedef typename table_type::const_iterator   const_iterator;
    private:
      table_type callback_map;
    public:
      bool erase(key_type key)
      {
        return (callback_map.erase(key) == 1);
      }
      bool insert(key_type key, data_type data)
      {
        return callback_map.insert(value_type(key,data)).second;
      }
      data_type find(key_type key) const
      {
        const_iterator cb_iter(callback_map.find(key));
        if(cb_iter == callback_map.end())
          throw std::runtime_error("key not found");
        return cb_iter->second;
      }
    };
    

    Bem.: Im public-Bereich der Klasse werden einige Typdefinitionen getätigt, um lästige Schreibarbeiten zu reduzieren. Außerdem finde ich, dass Ausdrücke wie std::map<key_type,data_type>::value_type den Source-Code ziemlich unleserlich machen. Des Weiteren benötigen wir natürlich Methoden zum Hinzufügen, Löschen und Finden von Einträgen.

    Schreiben wir gleich ein kleines Programm, um unsere Template-Klasse zu testen.

    #include <iostream>
    #include <vector>
    #include "register_table.h"
    using namespace std;
    
    void fun1(void)
    {
      cout << "fun1 " << endl;
    }
    void fun2(void)
    {
      cout << "fun2 " << endl;
    }
    void fun3(void)
    {
      cout << "fun3 " << endl;
    }
    int main()
    {
      typedef void (*fptr_type) (void);
      typedef register_table<int, fptr_type> my_table_type;
      my_table_type my_table;
      std::vector<int> cond_v;
    
      cond_v.push_back(2);
      cond_v.push_back(1);
      cond_v.push_back(3);
      cond_v.push_back(2);
      cond_v.push_back(2);
      cond_v.push_back(1);
      cond_v.push_back(3);
    
      my_table.insert (1,fun1);
      my_table.insert (2,fun2);
      my_table.insert (3,fun3);
    
      try
      {
        for (size_t idx = 0 ; idx != cond_v.size(); ++idx)
          my_table.find(cond_v[idx])();
      }
      catch(std::runtime_error & e)
      {
        cout << e.what() << endl;
      }
      return 0;
    }
    

    Die Ausgabe liefert das erwartete Ergebnis

    fun2 
    fun1 
    fun3 
    fun2 
    fun2 
    fun1 
    fun3
    

    4. Verallgemeinerung des Objekt-Switching Patterns

    4.1 OnError-Policy

    Die Methode find wirft eine Exception vom Typ std::runtime. Besser ist es natürlich, wenn der Benutzer unserer Template-Klasse festlegen kann, wie die Klasse im Fehlerfall reagieren soll. Um dies zu erreichen, müssen wir lediglich einen weiteren Template-Parameter hinzufügen. Wird keiner angegeben, soll sich die Klasse wie bisher verhalten.

    Für den Default-Fall stellen wir eine Implementierung mit einer statischen Methode zur Verfügung.

    struct DefaultError
    { 
      static void process_error() 
      {
        throw std::runtime_error("key not found");
      }
    };
    

    Da die Methode process_error() statisch ist, müssen keine Objekte erzeugt werden.

    template <typename Key, typename Data, typename OnError=DefaultError>
    class register_table
    {
    //..
      data_type find(key_type key) const
      {
        const_iterator cb_iter = callback_map.find(key);
        if(cb_iter == callback_map.end())
          OnError::process_error();
        return cb_iter->second;
    //... 
    }
    

    Bem.: Die Parameter-Liste einer Template-Klasse darf Default-Typen enthalten. Sie werden verwendet, falls kein Typ angegeben wird. Die Default-Typen dürfen nur am Ende der Parameter-Typ-Liste erscheinen.

    Der Benutzer der Template-Klasse hat nun die Möglichkeit, das Verhalten der Methode find von außen zu steuern. Solche Klassen werden in der Literatur als Policy-Klassen bezeichnet. Sie geben eine syntaktische Schnittstelle vor, die strikt eingehalten werden muss.

    struct IgnoreError
    {
      static void process_error() {} //do nothing 
    };
    //...
    typedef register_table<int,fptr_type,IgnoreError> my_table_type;
    

    Wenn die Methode process_error() keine Exception wirft, fehlt das Kriterium, ob der zurückgegebene Datensatz gültig ist. Deshalb ist die Verwendung der Klasse IgnoreError keine gute Idee, da nun das Gültigkeitskriterium auf andere Weise nachgeliefert werden muss. Wird der Default-Parameter OnError vom Aufrufer belegt, sollte er eine Error-Policy implementieren, die eine spezifische Exception wirft.

    4.2 Singleton-Muster

    Unsere Klasse soll so verändert werden, dass nur eine einzige Instanz erzeugt werden kann. Außerdem soll ein globaler Einstiegspunkt zur Verfügung stehen.

    • Klassen, die dieser Bedingung genügen, werden als Singleton-Klassen bezeichnet.

    Die einfachste Art, das Singleton-Muster zu implementieren, ist die Verwendung einer lokalen statischen Variablen. Dieses Design-Muster wurde erstmalig von Scott Meyers vorgeschlagen.

    Singleton & instance()
    {
      static Singleton obj;
      return obj;
    }
    

    Beim ersten Aufruf der Funktion wird das Objekt erzeugt und initialisiert. Wenn wir die Singleton-Funktion als statische Methode unserer Klasse implementieren, können wir den Konstruktor privatisieren. Auf diese Weise ist sichergestellt, dass nur eine Objektinstanz erzeugt werden kann. Der Anwender unserer Klasse kann somit keine Instanz der Klasse mehr erzeugen. Stattdessen verwendet er die Elementfunktion instance, um eine Referenz auf das Objekt zu erhalten.
    Damit eröffnet sich die Möglichkeit, Funktionspointer dezentral zu registrieren. Die Deklaration der zu registrierenden Funktionen musste bisher der Registrierungstabelle bekannt sein. Nun können wir einen anderen Weg einschlagen. Jedes Modul inkludiert die Registrierungstabelle und trägt die zu registrierende Funktion ein. Damit ist die Verantwortlichkeit an eine andere Stelle verschoben worden. Für die Initialisierung muss lediglich eine statische Dummy-Variable angelegt werden. Da statische Variablen vor dem Aufruf der main-Funktion initialisert werden, sind beim Programmstart alle Einträge in der Registrierungstabelle vorhanden. Ich hoffe, dass jetzt auch klar wird, warum die Methode insert einen boolschen Wert zurückliefert.

    // my_function.cpp
    #include "register_table.h"
    
    static bool dummy(  instance().insert(1,fun1)
                      &&instance().insert(2,fun2)
                      &&instance().insert(3,fun3)
                     );
    void fun1() {...}
    void fun2() {...}
    void fun3() {...}
    

    Bem.: Es gibt keine Festlegung in welcher Reihenfolge statische Variablen in den jeweiligen Modulen initialisiert werden.

    Diese Herangehensweise ist sehr wartungsfreundlich, da nicht mehr in vorhandenen Source-Code eingegriffen werden muss. Stattdessen können wir durch Hinzufügen von Modulen Funktionalitäten erweitern.

    4.3 Dummy Typ-Parameter

    Da wir das Singleton-Muster implementieren, können wir pro Typ genau eine Instanz erzeugen. Das beschränkt leider die Einsatzmöglichkeiten unserer Template-Klasse, da in manchen Programmen mehrere unabhängige Register-Tabellen desselben Typs benötigt werden. Wir lösen das Problem, indem wir unserer Template-Klasse einen zusätzlichen Default-Dummy-Pararmeter spendieren. Dadurch können wir beliebig viele Typen generieren, ohne die interne Datenstruktur der Registrierungs-Tabelle zu ändern.

    template <typename Key, typename Data, int Inst = 0, typename OnError=DefaultError>
    class register_table
    {
      //...
    }
    //...
    typedef register_table<int, fptr_type>   my_table_type1; //1. Instanz (Dummy-Template-Parameter)
    typedef register_table<int, fptr_type,1> my_table_type2; //2. Instanz
    typedef register_table<int, fptr_type,2> my_table_type3; //3. Instanz
    

    4.4 Implementierung des allgemeinen Objekt-Switching Patterns

    Nehmen wir nun alle Zutaten und erweitern unsere Template-Klasse um

    • die Error-Policy
    • das Singleton-Muster
    • und den Dummy-Typ-Parameter

    Natürlich müssen wir auch eine Implementierung der Default-Error-Policy zur Verfügung stellen. Die Template-Klasse könnte auf folgende Weise realisiert werden.

    #ifndef __register_table_4_02_05_2006__
    #define __register_table_4_02_05_2006__
    //----------------------------------------------------------------------------------------------
    #include <map>
    #include "default_policy.h"
    //----------------------------------------------------------------------------------------------
    template <typename Key, typename Data, int Inst = 0, typename OnError=DefaultError>
    class register_table
    {
    public:
      typedef Key key_type;
      typedef Data data_type;
      typedef std::map<key_type,data_type> table_type;
      typedef typename table_type::value_type value_type;
      typedef typename table_type::iterator   iterator;
      typedef typename table_type::const_iterator   const_iterator;
    private:
      table_type callback_map;
      register_table(){}
      register_table(const register_table &); //Deaktiviere Copy-Ctor
      register_table & operator=(const register_table &);
    public:
      ~register_table(){}
    //----------------------------------------------------------------------------------------------
      bool erase(key_type key) 
      {
        return (callback_map.erase(key) == 1);
      }
    //----------------------------------------------------------------------------------------------
      bool insert(key_type key, data_type data)  
      {
        return callback_map.insert(value_type (key,data)).second;
      }
    //----------------------------------------------------------------------------------------------
      data_type find(key_type key) const        
      {
        const_iterator cb_iter(callback_map.find(key));
        if(cb_iter == callback_map.end())
          OnError::process_error();
        return cb_iter->second;
      }
    //----------------------------------------------------------------------------------------------
      static register_table<Key,Data,Inst,OnError> & instance() 
      {
        static register_table<Key,Data,Inst,OnError> rt;
        return rt;
      }
    };
    //----------------------------------------------------------------------------------------------
    #endif
    

    5. Typschaltung globaler Funktionen

    Im ersten Anwendungbeispiel für unsere Template-Klasse werden globale Funktionen registriert und abhängig von einer Bedingung aufgerufen. Wir betrachten sowohl die Registrierung durch statische Initialisierung als auch die Registrierung innerhalb der main-Funktion. Außerdem nutzen wir die Möglichkeit, mehrere Instanzen der Registrierungstabelle zu erzeugen.

    #include <iostream>
    #include <vector>
    #include "register_table2.h"
    using namespace std;
    
    void fun1(void)
    {
      cout << "fun1 " << endl;
    }
    void fun2(void)
    {
      cout << "fun2 " << endl;
    }
    void fun3(void)
    {
      cout << "fun3 " << endl;
    }
    typedef void (*fptr_type) (void);
    
    typedef register_table<int, fptr_type,2> my_table_type2;   ///< Typdefinition der ersten Registrierungstabelle 
    #define __REG__(id,fun) (my_table_type2::instance().insert(id,fun)) ///< Makro, um Elemente in Reg. Tab einzufügen
    #define __EXEC__(id) (my_table_type2::instance().find(id)())        ///< Makro, um Funktion aufzurufen
    
    static bool dummy ( __REG__(1,fun3) &&  ///< Registrierung durch statische Initialisierung
                        __REG__(3,fun2) &&
                        __REG__(2,fun1) );
    int main()
    {
      typedef register_table<int, fptr_type,1> my_table_type; ///< Typdefinition der zweiten Registrierungstabelle
      std::vector<int> cond_v;    ///< Tabelle, um Bedingungen zu speichern
    
      cond_v.push_back(2); 
      cond_v.push_back(1);
      cond_v.push_back(3);
      cond_v.push_back(2);
      cond_v.push_back(2);
      cond_v.push_back(1);
      cond_v.push_back(3);
    
      my_table_type::instance().insert (1,fun1);  ///< Registrierungen zur Programmlaufzeit
      my_table_type::instance().insert (2,fun2);
      my_table_type::instance().insert (3,fun3);
    
      try
      {
        for (size_t idx = 0 ; idx != cond_v.size(); ++idx)
          my_table_type::instance().find(cond_v[idx])();   ///< Syntax wirkt wahrscheinlich eher abschreckend
    
        cout << "-----------------------------" << endl;
    
        for (size_t idx = 0 ; idx != cond_v.size(); ++idx)
          __EXEC__(cond_v[idx]);                           ///< einfach anzuwenden
      }
      catch(std::runtime_error & e)
      {
        cout << e.what() << endl;
      }
      return 0;
    }
    

    Die Makros __REG__ und __EXEC__ wurden lediglich zur Vereinfachung der Syntax eingeführt. Mit Hilfe der Makros können die Funktionen nun ganz einfach registriert und ausgeführt werden.

    Die Ausgabe liefert das erwartete Ergebnis

    fun2 
    fun1 
    fun3 
    fun2 
    fun2 
    fun1 
    fun3 
    -----------------------------
    fun1 
    fun3 
    fun2 
    fun1 
    fun1 
    fun3 
    fun2
    

    Unser Modul hat folgende Eigenschaften:

    • Es wird ein Typ-Identifizierer benötigt. Der Datentyp für den Typ-Identifizierer kann frei gewählt werden.
    • Zu jeder Bedingung muss eine Funktion geschrieben werden (Forced by Design).
    • Die Registrierungsklasse muss nichts über die zu registrierenden Funktionen wissen (Compiler-Abhängigkeiten).
    • Abläufe können einfach geändert werden (z.B. Abändern der Einträge von cond_v).
    • Gute Performance, da der Container std::map verwendet wird, der ein schnelles Suchen ermöglicht.
    • Der Container std::map benötigt mehr Speicher als ein einfaches Array.
    • Alle Elemente müssen dynamisch (zur Laufzeit) in den Container eingefügt werden.

    6. Typschaltung polymorpher Elementfunktionen

    Schauen wir uns noch ein weiteres Anwendungsbeispiel an. Diesmal verwende ich Funktoren aus der Loki-Bibliothek, um Elementfunktionen von polymorphen Objekten aufzurufen. Hauptaufgabe des Funktors ist die Speicherung von aufrufbaren Entitäten. Wie die Funktoren der Loki-Bibliothek im Detail funktionieren, möchte ich hier aber nicht erläutern, da dieses Thema Stoff für einen eigenständigen Artikel böte. Im Buch Modern C++ werden Funktoren sehr ausführlich beschrieben. Wenn ihr das Beispiel nachprogrammieren möchtet, könnt ihr die Loki hier downloaden. Ihr müsst dann entweder die Units smallobj.cpp und singleton.cpp aus der Loki-Bibliothek oder die Loki.lib zu eurem Projekt dazulinken.

    #include <iostream>
    #include <vector>
    #include <string>
    #include "register_table3.h"
    #include <Loki/Functor.h>
    using namespace std;
    //----------------------------------------------------------------------------------------------
    class Base  
    {
    public:
      Base() {cout << "ctor Base" << endl;}
      virtual void do_smth()  const {cout << "call Base::do_smth()" << endl;}
      virtual ~Base() {cout << "dtor Base" << endl;}
    };
    //----------------------------------------------------------------------------------------------
    class Derived : public Base
    {
    public:
      Derived() {cout << "ctor Derived" << endl;}
      virtual void do_smth() const {cout << "call Derived::do_smth()" << endl;}
      virtual ~Derived() {cout << "dtor Derived" << endl;}
    };
    //----------------------------------------------------------------------------------------------
    class AnotherDerived : public Base
    {
    public:
      AnotherDerived() {cout << "ctor AnotherDerived" << endl;}
      virtual void do_smth() const {cout << "call AnotherDerived::do_smth()" << endl;}
      virtual ~AnotherDerived() {cout << "dtor AnotherDerived" << endl;}
    };
    //----------------------------------------------------------------------------------------------
    template <typename Class> Class * instance() ///< schon wieder ein Singleton Pattern :-)
    {
      static Class obj;
      return &obj;
    }
    //----------------------------------------------------------------------------------------------
    // 1. Param -> Returntyp der Memberfunktion;
    // 2. Param (optional) Typliste mit Aufrufparametern der Memberfunktion;                   
    typedef Loki::Functor<void> PTMF_callback; 
    
    typedef register_table<int, PTMF_callback> reg_tab_polymorph; ///< Typdefinition der Registrierungstabelle
    //----------------------------------------------------------------------------------------------
    template <typename Class> bool Register(int id)
    {
      return reg_tab_polymorph::instance().insert(id,PTMF_callback(instance<Class>(),&Class::do_smth));  
    }
    //----------------------------------------------------------------------------------------------
    void Execute(int id)
    {
      reg_tab_polymorph::instance().find(id)(); 
    }
    static bool dummy(   Register<Base>(0)               ///< Registrierung durch statische Initialisierung
                      && Register<Derived>(1)
                      && Register<AnotherDerived>(2)
                     );
    
    //----------------------------------------------------------------------------------------------
    int main()
    {
      try
      {
        cout << std::string(40,'-') << endl;
        for (int idx = 0 ; idx != 3; ++idx)      
          Execute(idx);
        cout << std::string(40,'-') << endl;
      }
      catch(std::runtime_error & e)
      {
        cout << e.what() << endl;
      }
      return 0;
    }
    //----------------------------------------------------------------------------------------------
    

    Die Ausgabe liefert wieder das erwartete Ergebnis.

    ctor Base
    ctor Base
    ctor Derived
    ctor Base
    ctor AnotherDerived
    ----------------------------------------
    call Base::do_smth()
    call Derived::do_smth()
    call AnotherDerived::do_smth()
    ----------------------------------------
    dtor AnotherDerived
    dtor Base
    dtor Derived
    dtor Base
    dtor Base
    

    7. Erzeugung polymorpher Objekte

    Kommen wir nun zu einem letzten ausführlicheren Beispiel. Wie eingangs erwähnt können wir mit dem Object-Switching-Pattern auch eine Objekt-Fabrik implementieren. Es sollen Grafikobjekte aus einer Datei eingelesen und am Bildschirm dargestellt werden. Solche Problemstellungen werden nur selten in C++-Büchern abgehandelt, da sie etwas ausführlicher diskutiert werden müssen. Da es in diesem Artikel aber um dieses Thema geht, stellen wir uns dem Problem. Entwerfen wir also eine einfache Hierarchie von Grafikobjekten. Zunächst benötigen wir einige einfache Datentypen.

    //----------------------------------------------------------------------------------------------
    struct point 
    {
      int x;
      int y;
    };
    //----------------------------------------------------------------------------------------------
    struct gui_elem_file_s
    {
      int id;            ///< Identifizierer
      point origin;      ///< Ursprungs-Koordinaten
      const char * data; ///< inhomogene Daten
    };
    #endif
    

    Die Struktur point wird zum Speichern von Koordinaten benötigt. gui_elem_file_s ist eine Struktur, die in binären Files gespeichert wird. Im Element data werden die objektspezifischen Daten abgelegt.

    Als Basisklasse für alle Grafikobjekte dient die Klasse Shape. Sie ist nicht vollständig beschrieben, da sie nur die Elemente enthält, die für das Einlesen und Ausgeben von Shape-Objekten relevant sind.

    #ifndef __shape_04_06_2006_hdr__
    #define __shape_04_06_2006_hdr__
    #include <string>
    //----------------------------------------------------------------------------------------------
    struct gui_elem_file_s;  ///< Vorwärtsdeklarationen
    struct point;
    //----------------------------------------------------------------------------------------------
    class Shape
    {
    protected:
      point * origin_;      ///< Pointer auf Ursprungskoordinaten
    private:
      virtual void parse(const std::string & buffer) = 0;           ///< Objekt einlesen 
      virtual std::string & display(std::string & str) const = 0;   ///< Objekt ausgeben
      virtual std::string & whoami(std::string & str) const = 0;    ///< Wer bin ich
    public:
      Shape();                                             ///< ctor
      Shape(const Shape & obj);                            ///< copy ctor 
      Shape & operator=(const Shape & obj);                ///< Zuweisungsoperator     
      std::string & display_data(std::string & str) const; ///< NVI-Funktion
      void parse_data(const gui_elem_file_s & buffer);     ///< NVI-Funktion
      virtual ~Shape();
    };
    //----------------------------------------------------------------------------------------------
    #endif
    

    Um die Struktur point vorwärts deklarieren zu können, wird die Membervariable origin_ als Pointer implementiert. Aus dieser Entscheidung resultiert, dass wir den Konstruktor, den Copy-Konstruktor sowie den Zuweisungsoperator per Hand implementieren müssen. Die meisten Designer bevorzugen es, virtuelle Elementfunktionen privat zu deklarieren. Aus diesem Grund werden die öffentlichen, nicht virtuellen Elementfunktionen display_data und parse_data definiert. Die NVI-Elementfunktionen sind Wrapper für die privaten polymorphen Elementfunktionen. Diese Technik wird häufig als Nicht Virtuelles Interface (NVI) bezeichnet.

    std::string & Shape::display_data(std::string & str) const
    {
      std::string tmp;
      return whoami(str)
            .append(" x = ")
            .append(boost::lexical_cast<std::string>(origin_->x))  
            .append(" y = ")
            .append(boost::lexical_cast<std::string>(origin_->y))
            .append(" ")
            .append(display(tmp));
    }
    

    Bem.: Implementierung der NVI-Elementfunktion display_data. Sie klammert die privaten virtuellen Elementfunktionen whoami und display. Hätten wir nicht auf diese Technik zurückgegriffen, müssten wir die Ausgabe der Koordinaten in jede virtuelle Elementfunktion implementieren.

    Definieren wir nun noch einige von Shape abgeleitete Klassen. Ein Kreis ist sicherlich ein sinnvolles Zeichenobjekt. Jede von Shape abgeleitete Klasse definiert eine ID, die zum Identifizieren der Klasse dient.

    #include <string>
    #include "Shape.h"
    //----------------------------------------------------------------------------------------------
    class Circle : public Shape
    {
    public:
      enum {ID = 0};
    private:
      int radius_;
      //...
    };
    //----------------------------------------------------------------------------------------------
    

    Die Implementierungsdatei circle.cpp birgt keine Überraschungen mehr. Das Einzige, auf das es sich hier nochmals hinzuweisen lohnt, ist die Initialisierung der statischen Variablen dummy mit der Registrierungsfunktion.

    #include "circle.h"
    //.. weitere Includes
    
    static bool dummy(Reg<Circle>());  //Registrierung von Circle
    
    //.. Implementierungsdetails
    

    Die Registrierungsfunktion ist als Template implementiert und trägt die ID sowie die dazugehörige Creator-Funktion in die Registrierungstabelle ein. Außerdem müssen wir noch die Typdefinition für die Callback-Funktion und die Registrierungstabelle schreiben. Der Funktor speichert Zeiger auf Funktionen bzw. Methoden, die einen Pointer auf ein Shape-Objekt zurückliefern.

    //----------------------------------------------------------------------------------------------
    typedef Loki::Functor<Shape*> PTMF_callback; //Funktor der Shape-Erzeuger-Funktionen speichert
    typedef register_table<int, PTMF_callback> reg_tab_polymorph; //Typdefinition der Registrierungstabelle
    //----------------------------------------------------------------------------------------------
    template <typename Class> bool Reg()
    {rators für den Pointer 
      typedef Class *(*f_ptr)(void);
      f_ptr f_ptr_creator(&create<Class>); ///< Angabe des Adressoperators für Templatefunktionen
      return reg_tab_polymorph::instance().insert(Class::ID,PTMF_callback(f_ptr_creator));
    }
    //----------------------------------------------------------------------------------------------
    template <typename Class> 
    Class * create() 
    {
      return new Class;
    }
    //----------------------------------------------------------------------------------------------
    

    Bem.: Überraschenderweise muss der Adressoperator für den Pointer auf die Template-Funktion angegeben werden. Nicht-Template-Funktionen benötigen ihn nicht.
    Bem.: Die Registrierungsfunktion Reg musste ich für den Borland-Compiler umschreiben. Er konnte die Funktion nicht kompilieren, weil der Funktionspointer auf &create<Class> direkt als Argument für den Funktor übergeben wurde.

    Die Klasse Rectangle und Square sind ähnlich wie Circle implementiert. Bemerkenswert ist vielleicht, dass Square nicht von Rectangle (Ein Quadrat ist ein Rechteck), sondern von Shape abgeleitet ist. Dadurch wird ein Verstoß gegen das liskovsche Substitutionsprinzip (LSP) vermieden.

    Würde Square von Rectangle abgeleitet, könnte dies im Kontext eines Grafikprogramms falsch sein, da mit diesem üblicherweise grafische Elemente verändern werden können. So lässt sich z.B. bei Rechtecken die Länge der beiden Seiten unabhängig voneinander ändern. Für ein Quadrat gilt das jedoch nicht, denn nach einer solchen Änderung wäre es kein Quadrat mehr. Hat also die Klasse Rechteck Methoden wie set_width() oder set_height() (wurde im Beispiel zur besseren Übersicht weggelassen), so erbt Square die Methoden, obwohl deren Anwendung für ein Quadrat nicht erlaubt ist. Das LSP verlangt, dass die Bedeutung von Eigenschaften der Basis-Klassen in abgeleiteten Klassen nicht verändert wird.

    Nach soviel Vorbereitung sollten wir nun endlich den Vorhang für unsere Objekt-Fabrik aufmachen. Das Programm ist Gott sei Dank recht einfach gestrickt. Wir können uns also entspannt zurücklehnen. Das Array gui_elem_array[] simuliert die Datei, in der die Daten der Grafikobjekte gespeichert sind. Außerdem definiert das Hauptprogramm noch das Funktionsobjekt DeleteObj, das wir benötigen, um den Speicher für die Objekte mit Hilfe von std::for_each freizugeben.

    Kommen wir zum Programmablauf:

    • Das Array wird geparst. Abhängig von der ID wird eine in der Registrierungstabelle gespeicherte Creator-Funktion aufgerufen. Das dynamisch allokierte Shape-Objekt wird daraufhin im Vektor shape_v gespeichert.
    • Durch den Aufruf von parse_data werden die objektspezifischen Eigenschaften ermittelt und zugewiesen.
    • Schließlich müssen die Objekte mit display_data noch ausgegeben werden.
    • Bevor das Programm beendet wird, dürfen wir es nicht versäumen, die allokierten Grafikobjekte zu zerstören. Wem das nicht gefällt, kann alternativ Smart-Pointer-Objekte verwenden.
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include "shape_types.h"
    #include "shape.h"
    #include "shape_reg.h"
    using namespace std;
    using gfx::Shape;
    using gfx::gui_elem_file_s;
    using gfx::reg_tab_polymorph;
    //----------------------------------------------------------------------------------------------
    // gui_elem_array[] simuliert Datei, in der die Metainformationen über Grafikobjekte vorliegen.
    const gui_elem_file_s gui_elem_array[]  =
    {
       { 0 ,{ 5, 5}, "5"    } //Kreis mit Radius 5
      ,{ 1 ,{10,10}, "7;8"  } //Rechteck mit Breite 7 und Höhe 8
      ,{ 2 ,{20,15}, "3"    } //Quadrat mit Seitenlänge 3
      ,{ 1 ,{35,47}, "13;40"} //Rechteck mit Breite 13 und Höhe 40
      ,{ 2 ,{60,80}, "99"   } //Quadrat mit Seitenlänge 99
    };
    //----------------------------------------------------------------------------------------------
    Shape * Create(int id) ///< gibt registrierte Creator-Funktion auf Grafikobjekt zurück
    {
      return reg_tab_polymorph::instance().find(id)();
    }
    //----------------------------------------------------------------------------------------------
    struct DeleteObj ///< Funktionsobjekt zum Löschen von dynamisch allokierten Objekten
    {
      template <typename T> inline void operator() (T * object) const
      {
        delete object;
      }
    };
    //----------------------------------------------------------------------------------------------
    int main()
    {
      try
      {
        std::vector<Shape *> shape_v;
        std::string data;
        for(int idx = 0; idx != sizeof(gui_elem_array)/sizeof(gui_elem_file_s); ++idx)
        {
          shape_v.push_back(Create(gui_elem_array[idx].id)); ///< Füllt Vector mit Grafikobjekt-Pointer
          shape_v[idx]->parse_data(gui_elem_array[idx]);     ///< Grafikobjekte mit Daten füllen
          cout << shape_v[idx]->display_data(data) << endl;  ///< Grafikobjekte ausgeben
        }
        std::for_each(shape_v.begin(),shape_v.end(),DeleteObj()); ///< alle Grafikobjekt-Pointer löschen
      }
      catch(std::runtime_error & e)
      {
        cerr << e.what() << endl;
      }
      return 0;
    }
    //----------------------------------------------------------------------------------------------
    

    Bem.: alle Shape-Klassen und Funktionen liegen im Namensraum gfx

    Die Ausgabe zeigt die erzeugten Grafikobjekte.

    Circle    x = 5 y = 5 radius = 5
    Rectangle x = 10 y = 10 width = 7 height = 8
    Square    x = 20 y = 15 length = 3
    Rectangle x = 35 y = 47 width = 13 height = 40
    Square    x = 60 y = 80 length = 99
    

    Irgendwie finde ich das Beispiel cool. Obwohl die Header-Dateien für Circle, Rectangle und Square nicht in das main-Programm eingebunden sind, können Kreise, Rechtecke und Quadrate dargestellt werden. Das *main-*Programm weiß nicht einmal, dass diese Grafikobjekte existieren. Wenn es viele von Shape abgeleitet Klassen gibt, können durch diese Art der losen Kopplung die Zeiten für das Kompilieren eines Projektes drastisch reduziert werden.

    8. Schlusswort

    Ich hoffe, dass euch das Objekt-Switching-Muster gefallen hat. Vielleicht wandert das Design-Pattern ja in eure Trickkiste. Das Muster ist vielseitig einsetzbar und kann einiges an manueller Kodierungsarbeit ersparen. Die Codestellen habe ich mit dem Visual-C++-Compiler 2005 und dem Borland C++ Builder 6 übersetzt. Sie sollten aber mit jedem standardkonformen C++-Compiler kompiliert werden können. Den vollständigen Source-Code für das Objekt-Fabrik-Beispiel könnt ihr hier als Zip-Archiv downloaden.

    9. Referenzen

    Effektiv C++ programmieren | ISBN: 3827322979

    Effektiv C++ programmieren von Scott Meyers
    ISBN: 3827322979

    Effective STL | ISBN: 0201749629

    Effective STL von Scott Meyers
    ISBN: 0201749629

    Modernes C++ Design | ISBN: 3826613473

    Modernes C++ Design. Generische Programmierung und Entwurfsmuster angewendet von Andrei Alexandrescu
    ISBN: 3826613473



  • Haben sie sich das Pattern selbst ausgedacht oder warum findet man darüber in Google nichts? 🙂



  • ..... schrieb:

    Haben sie sich das Pattern selbst ausgedacht oder warum findet man darüber in Google nichts? 🙂

    Jein! Objekt-Fabriken gibt es ja bereits. Ich habe das Muster nur modifiziert, da ich mich nicht auf die Erzeugung von Objekten beschränken wollte.
    Den Begriff Object-Switching habe ich mir ausgedacht.

    Mit besten Grüssen

    rik



  • 3. codeteil in "3. Implementierung des Objekt-Switching Patterns "

    rik schrieb:

    const std::string & get_value(int key)
    {
      s_map::const_iterator s_iter = string_map.find(key);
      if(s_iter == string_map.end())
      {
        throw std::runtime_error("key not found");
      }
      return s_iter.second;
    }
    

    sollte es nicht

    return s_iter->second;
    

    heißen 😕

    bin noch nicht ganz durch aber bis jetzt super artikel 👍

    mfg.



  • s_map::const_iterator s_iter = string_map.find(key);
    

    Scheint mir kein Pointer zu sein, oder? 😉



  • nein, aber ein iterator.



  • Yupp, die werden in dem Fall wie Pointer behandelt.

    Und der Artikel ist wirklich sehr gut 👍 👍



  • Argh ... peinlich ...

    @GPC
    Naja, du weißt ja wie einem so etwas raus rutscht 😉



  • Cpt. Tanga schrieb:

    sollte es nicht

    return s_iter->second;
    

    heißen 😕

    Danke für die Info! War mein Fehler. Ein Iterator wird natürlich mit dem Pfeiloperator angesprochen. Ich korrigiere die Stelle im Artikel.



  • Sollte das:

    data_type find(key_type key) const
      {
        const_iterator cb_iter(callback_map.find(key));
        if(cb_iter == callback_map.end())
          throw std::runtime_error("key not found");
        return cb_iter->second;
      }
    

    Nicht so aussehen:

    const data_type& find(key_type key) const
      {
        const_iterator cb_iter(callback_map.find(key));
        if(cb_iter == callback_map.end())
          throw std::runtime_error("key not found");
        return cb_iter->second;
      }
    

    Ansonsten auch mal nen cooler Artikel 👍 Bin zwar noch net ganz durch aber trottdem.



  • Freak_Coder schrieb:

    Sollte das nicht so aussehen:

    const data_type& find(key_type key) const
      {
        const_iterator cb_iter(callback_map.find(key));
        if(cb_iter == callback_map.end())
          throw std::runtime_error("key not found");
        return cb_iter->second;
      }
    

    Der Datentyp data_type ist in den Beispielen ein Pointer auf eine Funktion bzw. einen Funktor. Das Kopieren von Pointern ist relativ billig, da nur eine Adresse zurückgeliefert werden muss.
    Ein Referenz sollte nur dann zurückgegeben werden, wenn der Aufwand für das Kopieren von Objekten groß ist.



  • rik schrieb:

    Der Datentyp data_type ist in den Beispielen ein Pointer auf eine Funktion bzw. einen Funktor. Das Kopieren von Pointern ist relativ billig, da nur eine Adresse zurückgeliefert werden muss.
    Ein Referenz sollte nur dann zurückgegeben werden, wenn der Aufwand für das Kopieren von Objekten groß ist.

    Achso, ok. Wenn das nur auf die Beispiele hier spezialisiert ist, dann ist das ja OK 😃



  • Freak_Coder schrieb:

    Achso, ok. Wenn das nur auf die Beispiele hier spezialisiert ist, dann ist das ja OK 😃

    Ähm! So war das nicht gemeint. Sinn und Zweck des Patterns ist es ja aufrufbare Entitäten zu speichern. Das sind nun mal Pointer oder Funktoren. Man könnte das ganze natürlich noch intelligenter machen und z. B. mit Type-Traits-Template-Klassen feststellen ob der Datentyp ein Pointer oder ein Objekt ist. Abhängig davon könnte man einen Rückgabe-Datentyp definieren. Man hätte auch einen zusätzlichen Template-Parameter einführen können, um den Benutzer entscheiden zu lassen, ob per Value oder per Referenz zurückgegeben wird.
    Ich habe das nicht gemacht, damit der Blick für das Wesentliche nicht verloren geht.



  • Ahh, OK 🙂



  • Sehr guter Artikel 🙂

    Gibts den Code auch irgendwo zum runterladen?



  • phlox81 schrieb:

    Sehr guter Artikel 🙂

    Gibts den Code auch irgendwo zum runterladen?

    Im Magazin : http://magazin.c-plusplus.net/print/Object-Switching.pdf



  • KasF schrieb:

    phlox81 schrieb:

    Sehr guter Artikel 🙂

    Gibts den Code auch irgendwo zum runterladen?

    Im Magazin : http://magazin.c-plusplus.net/print/Object-Switching.pdf

    Ich meinte eigentlich das fertig gestellte ObjectSwitching Template, als Datei. Nicht den ganzen Artikel 😉



  • KasF schrieb:

    phlox81 schrieb:

    Sehr guter Artikel 🙂

    Gibts den Code auch irgendwo zum runterladen?

    Im Magazin : http://magazin.c-plusplus.net/print/Object-Switching.pdf

    als pdf sehen die Artikel 1000 mal besser aus, als diese blöden HTML seiten.



  • funky cat schrieb:

    KasF schrieb:

    phlox81 schrieb:

    Sehr guter Artikel 🙂

    Gibts den Code auch irgendwo zum runterladen?

    Im Magazin : http://magazin.c-plusplus.net/print/Object-Switching.pdf

    als pdf sehen die Artikel 1000 mal besser aus, als diese blöden HTML seiten.

    Schau doch mal im Kapitel 8 des Artikels. Dort gibt's die Sourcen zum downloaden.


Anmelden zum Antworten