o Thorsten Pawletta, University of Wismar, Dep. of Mechanical-, Process- and E nvironmental Eng.,


Eine Klassenbibliothek zum persistenten Objektmanagement in C++

Autor : Jens- Uwe Dolinsky (andere Projekte)

e-mail : u.dolinsky@iname.com

Nähere Information über das Projekt können der folgenden Veröffentlichung entnommen werden:

Dolinsky J.-U. and Pawletta T.: A lightweight class library for extended persistent object management in C++, Software - Concepts & Tools, Volume 19 Issue 2 (1998) pp 71-79, Springer-Verlag Berlin Heidelberg.


Inhalt :
    1. Allgemeines
    2. Leistungsmerkmale der Klassenbibliothek
    3. Zur Implementation
    3.1. Die charlist_typ - Klasse
    3.2. Die OBJECT_list- Klasse
    3.3. Die Nachrichtenparameterstruktur
    3.4. Die Klasse persistent
    3.4.1. Implementationsschnittstelle : die virtuelle Methode move_data()
    3.4.2. Persistenzmethoden
    3.4.3. Globale Objektgenerierung
    3.4.4. Symbolische Konstanten und Macros
    3.4.5. Die beiden Schnittstellenmethoden load und store
    3.5.Implementationsbeispiel
    4. Abstrakter Algorithmus
    5. Anleitung zur Nutzung
    6. Behandlung von Unterbäumen und Strukturvariabilität
    7. Ausblick und Hinweis
    Download : persis.tar.gz (18 KB)


1. Allgemeines

C++ bietet vom Sprachstandard im Gegensatz zu anderen Programmiersprachen wie Smalltalk keine Lösung zum persistenten Speichern von Klasseninstanzen.

Diese Dokumentation stellt die Implementation und Funktionsweise eines allgemeinen, effizienten Persistenzmechanismus für C++- Klassen dar. Realisiert wurde dieser durch eine generische Basisklasse "persistent", die alle benötigten Datenstrukturen und Methoden kapselt. Durch Erben dieser Basisklasse können jeder beliebigen Klasse diese Mechanismen zum Speichern/Laden von unterschiedlich typisierten Memberdaten verfügbar gemacht werden. Diese Memberdaten können Inkarnationen skalarer oder strukturierter Datentypen sein. Dynamische Daten können ebenfalls verwaltet werden, doch gibt es hierbei einige Besonderheiten zu beachten, dazu später aber mehr. Es ist möglich, beliebig verkettete Strukturen (die auch Zyklen enthalten können) abzuspeichern bzw. zu rekonstruieren.


2. Leistungsmerkmale der Klassenbibliothek


3. Zur Implementation

Die Realisierung besteht aus drei Klassen- und einer Strukturdefinition:

  1. charlist_typ: privater Listentyp
  2. OBJECT_list: privater Listentyp
  3. persis_paramtyp: privater Nachrichtenparameter
  4. persistent: generische Basisklasse

3.1. Die charlist_typ- Klasse

struct charlist_element
{
	char    *value;
	struct charlist_element *next;
};

class charlist_typ
{
	charlist_element *root;
public:
	charlist_typ() { root = NULL;}
	~charlist_typ();
	void create(char* &s,unsigned l);
	void kill(char* s);
};

Die Klasse charlist_typ implementiert eine einfach verkettete, lineare Liste von Zeigern auf dynamisch allokierte Speicherbereiche. Ein Listeneintrag enthält dabei einen Zeiger auf den allokierten Speicherbereich und einen Verweis auf den nächsten Listeneintrag. Die Klasse kapselt als einzige Membervariable einen Zeiger auf das erste Listenelement. Mit Aufruf der Methode create(s,l) wird eine l Bytes große dynamische Variable erzeugt und deren Adresse über den Parameter s zurückgegeben. Zusätzlich wird diese Adresse in der inhärenten Liste registriert.

Durch den Aufruf der Methode kill(s) wird der durch s referenzierte Speicherbereich nur dann freigegeben, sofern s in der Liste registriert wurde.

Bei Finalisierung einer charlist_typ- Instanz wird durch deren Destruktoraufruf die gesamte, sofern vorhandene Liste gelöscht. Dabei werden auch alle durch die Listenelemente referenzierten Speicherbereiche für die dynamischen Variablen freigegeben.

Diese Eigenschaft (Garbage Collection) wird in der Klasse persistent genutzt, welche eine Instanz der Klasse charlist_typ als Membervariable kapselt. Anhand dieser Liste erkennt der Persistenzmechanismus, ob dynamische Variablen durch ihn erzeugt wurden oder nicht. Das folgende Klassendefinition veranschaulicht das Problem:



class bsp
{      
	char* name
	bsp(char* s) {name = s;}
};


Eine Instanz dieser Klasse initialisiert sein Element <name> mittels Konstruktoraufruf mit einem Zeiger auf einen String. Beim persistenten Speichern dieser Instanz wird dieser String mitgesichert. Vor dem Laden muß jedoch zuerst Speicher für den zu ladenden String reserviert werden. In der Konsequenz enthält die Instanz einen Zeiger auf eine lokale, dynamische Variable, deren Speicherplatz von der Instanz nicht verwaltet wird. Beim Finalisieren der Instanz entsteht dann ein Memoryleak. Das Problem tritt auch bei mehrfachen Reinitialisierungen auf. Würde eine Instanz mehrfach reinitialisiert werden, entstünden die Memoryleaks schon nach der zweiten Reinitialisierung. Diese Probleme lassen sich mit der implementierten Garbage- Collection vermeiden.

3.2 Die OBJECT_list- Klasse


struct olist_element /*list element type of OBJECT_list*/
{
	int number; /*position number in OBJECT_list*/
	persistent *object_ptr; /*pointer of persistentobj.*/
	struct olist_element *next; /*pointer of next list element*/
};

class OBJECT_list /*list of read or written data*/
{
	olist_element *list_anchor; /*pointer of first list element*/
	olist_element *current_position; /*pointer of last list element*/
	int quantity; /*number of elements in list */
public:
	OBJECT_list();
	~OBJECT_list();
	void insert(persistent*); /*insert element in list*/
	int number_of(persistent*); /*get position number of element*/
	persistent *object(int); /*get pointer of persistent obj.*/
	void clear_list(); /*delete all list elements*/
};

Die Klasse OBJECT_list wird vom Persistenzmechanismus verwendet, um vor allem bei zyklischen, rückwärts verketteten oder ringförmigen Strukturen zu erkennen, welche Objekte schon geladen bzw. gespeichert worden sind. Sie ist Hilfsmittel des Objektmanagements der generischen Klasse .

Die Klasse ist ein einfach verketteter, linearer Listentyp, der Referenzen auf Instanzen eines von der Klasse persistent abgeleiteten Typs verwaltet. Ein Listeneintrag besteht dabei aus der Referenz auf das Objekt, einer Variable für die von der Liste zugeordneten Identifikationsnummer und einem Verweis auf den nächsten Listeneintrag. In der Memberdatenstruktur der Klasse befinden sich jeweils ein Zeiger auf das erste und letzte Listenelement sowie eine Variable für die Summe der Listeneinträge. Durch Aufruf der Methode insert(element) wird ein neuer Listeneintrag für element generiert, der dabei eine eindeutige Identifikationsnummer (ID) zugeordnet bekommt. Über die Methode object(number) kann man die Referenz des Objektes mit der Identifikationsnummer number erhalten. Ist das Objekt nicht in der Liste, wird ein Null- Zeiger zurückgeliefert. Mit der Methode number_of(object) kann die ID des durch object referenzierten Objekts erfragt werden. Findet sich für dieses Objekt kein Eintrag in der Liste, wird 0 zurückgegeben. Ist object ein Null- Zeiger, wird -1 geliefert. Initialisiert wird eine Instanz dieser Klasse mit einer leeren Liste.

3.3. Die Nachrichtenparameterstruktur

Alle Informationen, die Objekte für ihre persistenten Aktionen brauchen, sind in der Struktur persis_paramtyp zusammengefaßt. Am Anfang einer persistenten Aktion wird eine(!) Instanz dieser Struktur erzeugt, entsprechend des Lade- oder Speichervorgangs initialisiert und danach an alle betroffenen Objekte als Nachrichtenparameter weitergereicht.

Das Flag to_disk zeigt dem benachrichtigten Objekt an, was mit seinen Memberdaten geschehen soll. Das gesetzte Flag bezeichnet den Speichervorgang, das nicht gesetzte den Ladevorgang.

Das Element file repräsentiert die Datei für die persistenten Daten der Objekte. Beim Speichervorgang nimmt sie die Daten auf, beim Laden werden die Objekte aus den Dateidaten rekonstruiert.

Das Element current_olist ist eine Instanz der Listenklasse OBJECT_list (aus Abschnitt 3.2). Jedes von einer persistenten Aktion betroffene Objekt trägt seine Referenz in diese Liste ein (Stichwort: zyklische Strukturen) und kann durch sie auch Informationen über Objekte erfragen, die vom Persistenzmechanismus schon behandelt worden sind.

Die Struktur persis_paramtyp ist der einzige Schnittstellenparametertyp des gesamten Persistenzmechanismus der Basisklasse persistent (Abschnitt 3.4) für die nach außen transparente Kommunikation zwischen den Objekten.

Vor Verwendung muß die Instanz einer solchen Struktur initialisiert werden. Dabei wird durch Setzen des Flags to_disk die gewünschte Betriebsart (Laden bzw. Speichern) festgelegt und entsprechend eine Datei geöffnet (nach Betriebsart jeweils ausschließlich zum Lesen bzw. Schreiben), auf deren Dateisteuerblock das Element file verweist. Das Listenelement current_olist initialisiert sich bei der Inkarnation durch Konstruktoraufruf selbständig mit einer leeren Liste.


struct persis_paramtyp
{
        BOOL            to_disk;        /* flag to sign if to read or to write*/
        FILE            *file;          /* file descriptor                    */
        OBJECT_list     current_olist;  
/* while loading or storing it contains references to*/
/* the stored/loaded Objects*/
};


3.4. Die Klasse persistent

Um einer beliebigen Klasse persistente Fähigkeiten zu verleihen, muß man ihr bei diesem Entwurf zuerst die Eigenschaften der Klasse persistent vererben.


class persistent
{
 /*private memberdata and -methods*/
private:        charlist_typ    dynamicstringlist;
 /*garbagecollector: to register all by persistent created strings*/
                persis_paramtyp *persis_para;

        void store_instance(persistent*);  /*stores the parameterinstance*/
        persistent      *get_instance(persistent*,BOOL);
        persistent      *p_create_Instance(char *name);
 /*########  the only Communicationmethod to other persistent- derivates  */
void p_message(persis_paramtyp*,char*); /*store obj. in file*/
public:
        int save (char *,char*,...);  /* Interface- methods*/
        int load (char *,char*,...);

protected:
        persistent(){}
        persistent(persistent *){}
 /*special constructor only used by creating derivates by persistentmechanism*/
virtual ~persistent(){}

DEF_GETCLASSNAME_FUNC(persistent) 
 /*Macro: creates the p_get_classname()- method, this function will be 
 created for derived classes implicitly by macro MOVE_DATA*/

virtual void move_data(){} 
 /*capsulate data methods, for this generic class it's empty */
 /*following methods are only for use in move_data - methods;
 dynamic_object(persistent**,persistent*,BOOL) macro, listed here only
 because of completeness */
void static_object(persistent &); /*for static persistent derivates*/
void static_string(char*);
void data(void *,unsigned);
/*for universal static datatypes; is used by several macros*/
void dynamic_string(char* &n,BOOL=true);/* for 0- terminated char* */
void dynamic_data(char* &,unsigned,BOOL=true);
 /*for dynamic data, notinstances of classes*/
void ptr_to_const(char *&,BOOL=true);
void dynamic_obj(persistent **,persistent*, BOOL=true);
void dynamic_template_obj(persistent **,persistent*,char*, BOOL=true);
 /*next function is for this generic class meanless derivated class
   overwrites this method implicitly by using the Macro MOVE_DATA*/
virtual void move_memberdata_of(char*)
  { PERSIS_EXCEPTION(ERR_UNKNOWN_CLASS);}
};


Als zentrale Klasse enthält sie die Basismethoden und Datenstrukturen für den eigentlichen Persistenzmechanismus. Durch sie werden die Verfahren zur Behandlung verschiedener einfacher und komplexer Datentypen implementiert. Diese Mechanismen berücksichtigen sowohl statische als auch dynamische Membervariablen. Das Nutzerinterface der Klasse beschränkt sich nur auf die beiden Methoden load() und save(). Durch deren Aufruf wird der Lade- bzw. Speicherprozeß für die jeweilige Instanz angestoßen.

Diese beiden Methoden sorgen dafür, daß eine Datei für Ein- bzw. Ausgabe geöffnet sowie eine Instanz der Struktur persis_paramtyp erzeugt und entsprechend der Betriebsart (Laden oder Speichern) initialisiert wird. Anschließend senden sie diese Struktur als Parameter der Methode p_message() an das aufrufende Objekt. Dadurch wird dessen virtuelle Methode move_data() aufgerufen, welche die Implementationsschnittstelle zum Persistenzmechanismus der Klasse persistent ist.

In abgeleiteten Klassen wird diese Methode redefiniert, indem in ihrem Rumpf für alle Membervariablen die Aufrufe der jeweils zum Typ passenden Persistenzmethoden (data(...), static_data(...), usw.) aufgereiht werden.

Jede Persistenzmethode (Abschnitt 3.4.2) stellt bei ihrem Aufruf an der Membervariable persis_para die Betriebsart des Persistenzmechanismus fest, und führt die entsprechende Aktion (Laden bzw. Speichern) durch. Ist ein Memberelement selbst eine von persistent abgeleitete Klasseninstanz, so sendet die Persistenzmethode (z.B. static_object(...)) mittels Aufruf von p_message(...) eine Nachricht an das Objekt, wodurch in diesem der beschriebene Vorgang angestoßen wird.

3.4.1. Implementationsschnittstelle : die virtuelle Methode move_data()

Die Methode move_data() "beschreibt" dem Persistenzmechanismus die Memberdatenstruktur der jeweiligen persistent- abgeleitete Klasse. Enthält die Klasse keine für eine Wiederherstellung des Zustandes wichtigen Daten, ist der Funktionsrumpf leer oder es wird evtl. die move_data - Methode des Vorgängers aufgerufen.

Sonst enthält sie die Aufrufe der Persistenzmethoden (Abschnitt 3.4.2) für alle wichtigen Memberdaten, wobei die Reihenfolge keine Rolle spielt. Befindet man sich schon höher in der Ableitungshierarchie, so darf man auch die Datenfelder der geerbten Klassen nicht vergessen. Es muß also die move_data()- Methode des Vorgängers explizit aufgerufen werden. Der Aufruf kann an beliebiger Stelle, auch zwischen Aufrufen der Persistenzmethoden, erfolgen.

ACHTUNG: Der Kopf der move_data()- Methode wird implizit durch das Macro MOVE_DATA (siehe dort) erzeugt.

3.4.2. Persistenzmethoden

Die Persistenzmethoden werden von der Klasse persistent für unterschiedlich typisierte, dynamische oder statische Memberdaten bereitgestellt. Sie senden den Daten im objektorientierten Sinne Nachrichten, die sie je nach Zustand des Flags persis_para->to_disk auffordern, sich mit Hilfe der Datei persis_para->file zu Laden bzw. zu Speichern.

  1. data(void*p,unsigned n) Elementarste Methode. Sie schreibt / liest <n> Bytes aus/in Speicherbereich <p>. Sie kann für Instanzen aller Typen (außer Klassen) verwendet werden.
  2. static_string(char* s) Behandelt den String <s>. Erst wird die Lange des Strings (gespeichert bzw. geladen), danach wird der String gespeichert bzw. geladen)
  3. dynamic_data(char *&,unsigned,bool) Sie dient zur Behandlung dynamischer Datentypen. Der erste Parameter ist der Zeiger auf den Speicherplatz, der zweite gibt die Größe desselben an. Der 3. Parameter ist ein Flag, mit welchem die Information übergeben wird, ob der Speicherplatz vor jedem Laden erzeugt werden soll (true LOAD_AFTER_CREATING) oder nicht (false LOAD_WITHOUT_CREATING).
  4. dynamic_string(char*&,bool) Es werden dynamische Strings gespeichert. Die Funktion des Flags entspricht der des Flags der Methode dynamic_data(...).
  5. ptr_to_const(char*&,bool) Sie dient zum Handling von Zeigern auf Zeichenketten (char*),die durch Konstruktoren mit Adressen von Konstanten initialisiert wurden (Problem siehe Abschnitt 3.1). Die von dieser Methode behandelten Strings werden durch die Garbage- Collection registriert. Die Funktion des Flags entspricht der des Flags der Methode dynamic_data(...).
  6. static_object(persistent&) Es werden statische persistent- Derivate behandelt. Dem durch die Referenz übergebenen Objekt wird die Nachricht zum Speichern bzw. Laden gesendet.
  7. dynamic_obj(persistent**,persistent*,bool) Fordert die referenzierte, von persistent abgeleitete Instanz auf, sich zu laden bzw. sich zu speichern. Der erste Parameter ist die Referenz des Zeigers, der auf die zu behandelnde Instanz verweist. Der zweite Parameter ist die Referenz der zu behandelnden Instanz. Der Grund für diese redundante Parameterübergabe ist die Realisierung von Typsicherheit bei der Programmierung. Weil beim Methodenaufruf durch ein Makro erzeugt wird, welches den ersten Parameter auf persistent** casted, könnten die Methode auch für andere Memberdaten verwendet werden, was dann einen u.U. schwer zu entdeckenden Programmlaufzeitfehler nach sich ziehen könnte. Damit solche Fehler vermieden werden, wurde die Parameterliste erweitert. Die Fehlerhafte Anwendung des Makros (siehe unten) wird so schon durch den Compiler entdeckt. Der dritte Parameter (Flag) hat die gleiche Funktion wie bei 4. und 5. .
  8. dynamic_template_obj(persistent**,persistent*,char*,bool) Es wird die Funktionalität von dynamic_template_obj(...) auf Templates erweitert. Dazu muß der Name des Parametertyps als 3. Parameter vom Typ char* übergeben werden.

3.4.3. Globale Objektgenerierung

Um bei der Rekonstruktion Instanzen aller im Projekt verwendeten, von persistent abgeleiteten Klassen erzeugen zu können, müssen diese Klassen dem Persistenzmechanismus bekanntgemacht werden. Dies geschieht durch eine Methode, die nach Kenntnis aller im jew. Projekt verwendeten Klassen definiert werden muß (Wenn dynamische Strukturen verwendet werden).

Diese Methode ist wie folgt definiert:

 persistent* persistent::p_create_Instance(char *name)

Sie erzeugt eine Instanz der Klasse, deren Name mittels Parameter name als String übergeben wird.

Beispiel: In einem Projekt wurden 3 Persistent- Derivate(Klasse1, Klasse2, Klasse3) definiert. Dann muß die p_create_Instance()- Methode wie folgt definiert werden :

persistent *persistent::p_create_Instance(char *name)
{ if (strcmp(name,"Klasse1")==0) return new Klasse1((persistent*)NULL);else
  if (strcmp(name,"Klasse2")==0) return new Klasse2((persistent*)NULL);else
  if (strcmp(name,"Klasse3")==0) return new Klasse3((persistent*)NULL);else
   return NULL;
}


Weil diese Definition sehr aufwendig sein kann, wurde hierfür Makros definiert (siehe auch unten). Mit deren Nutzung sieht die Methodendefinition wie folgt aus:


DEF_CREATE_INSTANCE(REGISTER(Klasse1)
                    REGISTER(Klasse2)
                    REGISTER(Klasse3))

3.4.4. Symbolische Konstanten und Macros

Zur Vereinfachung der Anwendung und besseren Lesbarkeit der Quelltexte wurden verschiedene symbolische Konstanten und Makros definiert.
Die folgende Liste gibt einen Überblick darüber:

Flagwerte für die Persistenzmethoden 4.,5.,7. und 8.

Zur Vereinfachungen der Persistenzmethodenaufrufe

wurden verschiedene Macros definiert:

  1. static_data(arg) Wert: data((void*)&arg,sizeof(arg)). Dieser Makro kann für alle möglichen stati-schen Daten (außer Klasseninstanzen) verwendet werden.
  2. static_field(arg) Wert: data((void*)arg,sizeof(arg)). Dieser Makro kann zum Laden/Speichern von verschiedenen statischen C- Feldern der Form typ feld[anzahl] verwendet werden
  3. dynamic_object(object,create) Wert: dynamic_obj((persistent**)&object, object, with_creating ) Es wird für den Zeiger object (auf eine von persistent abgeleitete Instanz) der Persistenzmethodenaufruf generiert. Fehler beim Einsatz des Makros auf andere Zeiger können durch die redundante Parameterübergabe vom Compiler erkannt werden.
  4. dynamic_template_object(object,name,create) Erzeugt den Persistenzmethodenaufruf für object ( dynamic_template_obj((persistent**)&object, object,name, create) )

Makros für die globale Objektgenerierung


DEF_CREATE_INSTANCE(parameters) persistent *persistent::
p_create_Instance(char *name)\
{
        parameters\
return NULL;\
}

Erzeugt den Kopf und den Rumpf der Methode

REGISTER(typ)   if (strcmp(name,#typ)==0) return new\
typ((persistent*)NULL); else


Zentraler Makro für die Klassenanpassung

Der wohl wichtigste Macro MOVE_DATA(classname,predecessor) ist für jeden Persistenterben zu definieren.

Er hat 2 Argumente: classname ist der Klassennamen der Klasse, für die die Methode definiert werden soll; predecessor ist der Klassename der Klasse, von die aktuelle Klasse abgeleitet werden soll.

Der Macro erzeugt eine speziellen Konstruktor, der nur vom Persistenzmechanismus genutzt wird, die p_get_classname()- Methode (implizit durch Macro DEF_GETCLASSNAME_FUNC). Weiterhin erzeugt der Macro eine virtuelle Methode void move_memberdata_of(char* name). Mit dieser Methode wird es möglich, nur die Memberdaten einer geerbten Klasse zu behandeln. Es wird der Argumentstring mit dem aktuellen Klassennamen verglichen. Stimmen beide überein, wird die entsprechende move_data()- Methode aufgerufen, anderenfalls wird die Vorgängermethode aufgerufen und der Vergleich beginnt erneut. Abschließend generiert der MOVE_DATA- Macro den Funktionskopf der move_data()- Methode. Der Methodenrumpf für die move_data()- Methode kann also gleich angefügt werden (siehe Beispiel).

Dieser Macro soll dem Nutzer ebenfalls die formale Arbeit abnehmen und den Quelltext besser lesbar machen.

MOVE_DATA(classname,predecessor) classname(persistent* p):predecessor(p){}\
                                                DEF_GETCLASSNAME_FUNC(classname)\
virtual void move_memberdata_of(char* name)\
{
 if (strcmp(classname::p_get_classname(),name)==0)\
                classname::move_data();\
        else predecessor::move_memberdata_of(name);\
}\
virtual void move_data()



Beispiel:

class klasse1: public persistent
{
 public:
        int     member1;
        float   result;        //zwei willkürliche Memberobjekte

                MOVE_DATA(klasse1,persistent)
                {
                        persistent::move_data()     //Vorgaengerdaten behandeln
                        static_data(member1);
                        static_data(result);
                }
};



wird vom Präprozessor übersetzt in:

        class klasse1: public persistent
        {
        public:
                int     member1;
                float   result;

//Durch Macro erzeugt
        klasse1(persistent* p):persistent(p){}    //spezieller Konstruktor
        virtual char* p_get_classname() {return "klasse1";}
//liefert Klassennamen
        virtual void move_memberdata_of(char* name)
        { if (strcmp(klasse1::get_classname(),name)==0)
                klasse1::move_data();   //wenn name==Klassenname dann die
                                //klassenspezielle Methode klasse1::move_data()
                else persistent::move_memberdata_of(name);
//sonst dasselbe nochmal mit der Vorgaengerklasse
        }
        virtual void move_data()        //eigentliche move_data()-Definition
{      persistent::move_data() //Vorgaengerdaten behandeln
                data((void*)&member1,sizeof(member1)); //Ersetzungen siehe oben
                data((void*)&result ,sizeof(result));
        }
};





3.4.5. Die beiden Schnittstellenmethoden load() und store()

Ausgelöst wird der Persistenzmechanismus nur durch Aufruf die beiden Schnittstellenfunktionen load() für Laden aus der datei und store() zum Speichern. Beide habe die gleiche Parameterliste:

  void persistent::save(char *name,char *classname,...)
  void persistent::load(char *name,char *classname,...) 
  // open File for readin

char *name
gibt den Namen der Datei an, in/aus welcher das entsprechende Objekt gespeichert/geladen werden soll: Der Parameter muß(!) unbedingt angegeben werden und darf nicht NULL sein
char *classname
gibt den Klassennamen der Klasse an, deren Memberdaten gespeichert werden sollen. Bei tieferen Ableitungshierarchien kann es vorkommen, daß nur die Daten einer Basisklasse relevant sind und gespeichert werden muessen. In diesem Fall enthält <classname> den Namen dieser Basisklasse. Sollen alle Memberdaten der Instanz gespeichert werden (Normalfall), ist dieser Parameter NULL.
Die Ellipse
nimmt Pointer auf andere Objekte auf, die nicht vom Persistenzmechanismus berücksichtigt werden sollen. Dies ist zum Beispiel notwendig, um Teilstrukturen zu speichern, die Zeiger auf Parentobjekte haben. Es können mehrere Pointer angegeben werden. Abgeschlossen werden muß die Parameterliste mit NULL!!!

Alle anderen Funktionen können und dürfen von außen nicht aufgerufen werden.

Rückgabewerte der Interface- Methoden

Die Rückgabewerte sind Ergebnisse der auf dem Exception- Handling basierenden Fehlerbehandlung. Wird die Basisklasse ohne die symbolische Konstante WITH_EXCEPTIONS kompiliert, ist die Fehlerbehandlung deaktiviert. Das heißt, daß auch fehlerhafte Aktionen des Persistenzmechanismus keinen darauf hinweisenden Rückgabewert erzeugen. Schlimmstenfalls kann das Programm abstürzen.

3.5.Implementationsbeispiel

struct tstruct
{ float b;
  char sname[10];
};

class test:public persistent
{ public : //der Einfachheit wegen ist alles public
 int intvec[10];
 struct tstruct *ptstuct;
 char name[20];
 long ldat;
 test *neighbour;
 char *constname;
 test() { constname="constant name"}
MOVE_DATA(test,persistent)
{ static_field(intvec); //statisches Feld
  dynamic_data((char*)ptstruct,sizeof(tstruct),LOAD_AFTER_CREATING);
        //Zeiger muß laut Definition auf char* gecasted werden. 
        //Beim Laden wird Speicherplatz für die Variable erzeugt
  static_string(name);//alternativ wäre static_field möglich,
                    //weil name ein statisches Feld ist
  static_data(ldat);
  dynamic_object(neighbour,LOAD_AFTER_CREATING);
  ptr_to_const(constname);//costname wird durch Zeiger auf eine Stringkonstante initialisiert
}
};

4. Abstrakter Algorithmus

Beim Speichern:

Die Instanz wird aufgefordert, ihre Datenfelder in Datei "testdatei.persistent" zu speichern.


1. CALL INSTANZ.save("testdatei.persistent")
2.      ->Öffnen der Datei
3.      INSTANZ.para.file = Dateihandle
4.      INSTANZ.para.to_disk- Flag == Schreibmodus
5.      CALL INSTANZ.p_message- Methode
6.              IF (Instanz schon gespeichert) THEN 
7.                      schreibe Objektnummer
8.                      RETURN
9.              END IF
10.             -> Eintragen der Adr. von INSTANZ in object_list- Liste
11.             CALL INSTANZ.move_data- Methode
12.                     FOR (alle persistent- Derivate)
13.                             Schreiben des Klassennamen
14.                             CALL INSTANZ.derivat.p_message(INSTANZ.para)
15.                     ENDFOR
16.                     FOR (alle sonstige Memberdaten)
17.                             CALL (zugehörige Persistenzmethode)
18.                     ENDFOR
19.     Datei file schliessen;



Beim Laden:

Die Instanz wird aufgefordert, ihre Datenfelder aus Datei "testdatei.persistent" zu reinitialisieren.


1. CALL INSTANZ.load("testdatei.persistent")
2.      ->Öffnen der Datei
3.      INSTANZ.para.file = Dateihandle
4.      INSTANZ.para.to_disk- Flag == Lesemodus
5.      CALL INSTANZ.p_message- Methode
6.              IF (Instanz schon geladen) THEN
7.                      RETURN
8.              END IF
9.              -> Eintragen der Adresse von INSTANZ in Objektliste
10.             CALL INSTANZ.move_data- Methode
11.                     FOR (alle persistent- Derivate)
12.                             -> Erzeugen von INSTANZ.derivat
13.                             CALL INSTANZ.derivat.p_message(INSTANZ.para)
14.                     ENDFOR
15.                     FOR (alle sonstige Memberdaten)
16.                             CALL (zugehörige Persistenzmethode)
17.                     ENDFOR
18.     Datei file schliessen;


5. Anleitung zur Nutzung

Um Klassen persistente Eigenschaften zu geben, muß bei der Implementierung folgendes beachtet werden:

  1. #include "persis.h"
  2. Die jeweilige Klasse muß die Klasse persistent erben.
  3. Redefinieren der virtuellen Methode move_data() mittels Makro MOVE_DATA Diese Methode enthält die für den Persistenzmechanismus notwendigen Strukturinformation der Klasse in Form von Aufrufen der zum Typ der Memberdaten passenden Persistenzmethoden. Wenn die Klasse des zu speichernde Objekt mehrere Generationen von persistent entfernt ist, muß auch die Anweisung vorgaenger::move_data() eingefügt werden, damit alle Vorgängerklassen ihre Datenfelder abspeichern Können. Bei den Vorgängern muß die Methode move_data() implementiert sein. Wenn es Elemente gibt, die nicht persistent gespeichert werden brauchen, weil sie z.B. keinen Einfluß auf den Zustand des Objektes, können sie weggelassen werden.
  4. Definieren der p_create_Instance() Methode mittels Makro:

        DEF_CREATE_INSTANCE(    REGISTER(Klasse1)
                                REGISTER(Klasse2))

Nur einmal im gesamten Projekt

Beispiele:


#include "persis.h"
// Die Klasse test1 hat außer Daten keine Referenzen
// auf Objekte

typedef class test1 : public persistent
{
        char    name[10];
        int     a,b;
        float   c;
        struct  {
                int     f;
                char    t;} struktur;
        
        MOVE_DATA(test1,persistent)
        {
                static_data(a);  //Reihenfolge der Anweisungen beliebig
                static_data(b);
                static_data(c);
                static_data(struktur);
                static_field(name);
        }
                *
                *
};

// Klasse test2 hat Daten und Referenzen auf Memberobjekte vom Typ test2
typedef class test2 : public test1
{
        int             a,b;
        unsigned        c;
        class test2     *nachfolger;

        MOVE_DATA(test2,test1)
        {      static_data(a);
                static_data(b);
                static_data(c);
                test1::move_data(); // weil Erbe von test1
                dynamic_object(nachfolger,LOAD_AFTER_CREATING);
        }
                *
                *
};



Klasse test3 hat Referenzen auf Objekte vom Typ test2 und test1

typedef class test3 : public persistent
{
        test1 *inst1,*inst2;
        test2 *inst3;

        MOVE_DATA(test3,persistent)
        {
                dynamic_object(inst1,LOAD_AFTER_CREATING);
                dynamic_object(inst2,LOAD_AFTER_CREATING);
                dynamic_object(inst3,LOAD_AFTER_CREATING);
        }
        *
        *
};


DEF_CREATE_INSTANCE(    REGISTER(test1)
                                REGISTER(test2)
                                REGISTER(test3))
//Implementation der p_create_Instance()- Methode



Zum eigentlichen Speichern muß nur die save(...)- Methode des zu des zu speichernden Objektes aufgerufen werden:

Beispiel:

h->save("pers_obj",NULL,para,NULL);

Die Instanz wird in die Datei "pers_obj" abgelegt (Erklärung zu para: siehe Behandlung von Unterbäumen). Vor dem Laden wird das Wurzelobjekt mittels new- Operator geladen. Danach wird die load(Dateiname)- Methode des zu ladenden Objektes aufgerufen:

Beispiel:

h = new test();
h->load("pers_obj",NULL,para,NULL);



Es ist auch möglich, nur die Memberdaten einer geerbten Klasse separat zu behandeln. Dabei muß in der save/load- Methode noch der jeweilige Klassenname angegeben werden.

h->save("pers_obj","test1",NULL); 

Von der Instanz vom Typ test2 werden nur die Memberdaten des geerbten Typs test1 gesichert.

6. Behandlung von Unterbäumen und Strukturvariabilität

Die Interfacemethoden load(char*,char*,...) und save(char*,char*,...) haben als 3. Parameter eine persistent* - Argumentenliste variabler Länge. Wird zum Beispiel nur ein Unterbaum persistent gespeichert, muß dafür gesorgt werden, daß alle Verweise auf die jeweilige Parentclass nicht durch den Persistenzmechanismus verfolgt werden.

Dies wird erreicht, indem die entsprechenden Pointer auf die nicht zu speichernden Instanzen der Interfacemethode übergeben wird. Die Argumentenliste wird abgeschlossen mit dem NULL- Pointer (zwingend notwendig wegen der Ellipse !!!).

Diese Adressen werden in die Object_list der bereits gespeicherten bzw. geladenen Instanzen eingefügt, und deshalb von da an als gespeichert bzw. geladen betrachtet. Auf die Weise kann ein Unterbaum ,in deren Instanzen beliebige Referenzen auf andere Instanzen ,die nicht zu behandeln sind, existieren, selektiv gespeichert/geladen werden.

Wurde ein Unterbaum herausgespeichert, muß beim Einladen die Parameterliste der load()- Methode dieselbe Anzahl Parameter haben, damit der Baum in das bestehende System eingefügt werden kann. Werden der Interfacemethode beim Laden der Instanz weniger Parameter übergeben als beim Speichern, werden die fehlenden Referenzen mit NULL initialisiert. Auf diese Weise ist es möglich, aus Strukturen herausgespeicherte Objekte autonom zu laden.

(siehe auch example2.cc)

7. Ausblick und Hinweis

Weil die Standarddatentypen auf unterschiedlichen Rechnern verschiedene Größen oder Formate haben können, ist der Austausch der von Persistent erzeugten Dateien unter diesen Plattformen nur dann sinnvoll, wenn sie vorher in ein geeignetes Zwischenformat konvertiert wurden. Das heißt, die data()- Funktionen müssen so erweitert werden, daß sie z.B. vor dem eigentlichen Speichern die Werte in vereinbarte Zwischenformate z.B. Strings konvertieren und dann erst ablegen.


Wismar, 23.Oktober 1997
u.dolinsky@iname.com