Forum: PC-Programmierung C++: "dynmisches" new durch Objekte aus XML-Datei. Möglich?


von Alexander I. (daedalus)


Lesenswert?

Hallo,

ich möchte folgendes erreichen:
Ein Programm, eine Art Textinterpreter von Skriptbefehlen, lädt eine 
XML-Datei in der dem Interpreter bekannte Objektarten definiert sind.

Die Objekte werden so abgebildet:
<objects>
  <Obj type="Obj1">
    <param type="uint8_t" name="p1" />
    <param type="uint8_t" name="p2" />
  </Obj>
  <Obj type="Obj2">
    <param type="uint8_t" name="p1" />
    <param type="char*" name="p2" />
  </Obj>
  <Obj type="Obj3">
    <param type="uint8_t" name="p1" />
    <param type="uint8_t" name="p1" />
    <param type="Obj1" name="p3" />
  </Obj>
</objects>

Dahinter verbirgt sich eine Liste von Objekten (Obj) die über 0 bis n 
Parameter verfügen. Die Parameter sind die zu übergebenden 
Konstruktorparameter des Objekts und können sowohl Basisdatentypen wie 
int, char usw. als auch Zeiger auf andere Objekte sein.

Jedes Objekt ist eine dem Interpreter bekannte C-Klasse, die ein Attibut 
besitzt, das identisch mit dem "type"-Attribut von Obj ist. So kann der 
Interpreter eine Verbindung zwischen C-Klasse und XML-Objekt herstellen.

Nun sollen durch das Interpreterskript zur Laufzeit Objekte erzeugt 
werden können, die in der XML-Datei beschrieben worden sind. Also z.B.
"Create Obj2("NameDerInstanz",p1,p2);"

Im Hintergrund verwertet der Interpreter dieses Skript vereinfacht 
beschrieben zu einem C++-typischen
1
obj2* MyObject = new obj2(p1,p2,"NameDerInstanz");
Der zu "Obj2" passende Klassenkonstruktor muß jetzt irgendwie (und 
genau das ist meine Frage) gefunden werden können, da der Konstruktor 
von Obj1 oder Obj3 nicht passt. Anschließend kann der Interpreter dieses 
Objekt dann eindeutig zuordnen und z.B. folgende Skriptoperationen 
ausführen:

obj("NameDerInstanz").Read(&data,25);

die intern so verarbeitet werden:
1
GetObjectFromList("NameDerInstanz")->Read(param1, param2);

Sonstiges:
Alle Objekte haben eine gemeinsame abstrakte Basisklasse, die 
POSIX-Methoden wie READ, WRITE usw. implementiert. Wie kann ich nun 
dieses
1
obj2* MyObject = new obj2(p1,p2,"NameDerInstanz");
während der Programmausführung erzeugen? Alles über switch-case-Ketten 
zu verknüpfen geht zwar, aber ist nicht sonderlich schön. Gibt es auch 
eine elegantere Möglichkeit?

von Peter (Gast)


Lesenswert?

Ich glaube nicht das das mit C/C++ geht, ausser mit dem einem grossen 
case construct.
Für soetwas ist .net oder java besser geeignet. Dort gibt Object aus 
"strings" generieren.

object = new GetClassOf("klassen name");
(ist nur ein beispiel, die methoden heisen dafür anders)

von localhost (Gast)


Lesenswert?

Factory Pattern

Dynamisch, also ohne Interpreter geht das nicht, da C++ keine Laufzeit 
Informationen mehr hat. Das heisst Du musst:

if (name == "KlasseXXX") obj = new KlasseXXX(...);

Eine dynamische Namensgebung der Instanz ist nicht möglich (insofern, 
dass sie im C++ Code dann nicht unter diesem Namen ansprechbar ist). Du 
müsstest einen eigenen Interpreter schreiben, quasi eine Scriptsprache à 
la Python.

(also unter dem Vorbehalt, Deine Frage richtig verstanden zu haben :-).

von Karl H. (kbuchegg)


Lesenswert?

Ich denke, du hast hier

> Jedes Objekt ist eine dem Interpreter bekannte C-Klasse,
> die ein Attibut besitzt, das identisch mit dem "type"-Attribut
> von Obj ist.

schon deinen Designfehler.

Was du bräuchtest, ist eine Klasse, die in der Lage ist alle die 
Objektkategorien, die du durch
  <Obj type="Obj1">
    <param type="uint8_t" name="p1" />
    <param type="uint8_t" name="p2" />
  </Obj>
bzw. deren Variationen beschreiben kannst, darzustellen.

Ansonsten:
Du suchst nach einem Factory-Pattern.
Jede Objektklasse registriert sich beim Hochfahren des Pgms. mit seinem 
Typ (und zb. einem template Objekt) bei der Factory. Wird ein Objekt 
eines bestimmten Typs benötigt, so durchforstet die Factory ihre Liste 
von bekannten Typen und erzeugt das benötigte Objekt (zb indem es das 
template Objekt auffordert eine Kopie von sich selbst zu erzeugen. 
Stichwort virtueller Copy Constructor, also eine virtuelle clone() 
Funktion).
Anstelle des template Objektes könnte man auch jeder Klasse eine 
statische create Funktion verpassen und die Factory speichert sich einen 
Pointer auf die zu jedem Typ gehörende create Funktion (ist vielleicht 
einfacher als über einen vCCtor zu gehen)


Aber ich denke ernsthaft, dass der ganze Ansatz mit "Das Programm hat 
für jede im XML beschriebene Objekt-Kategorie eine Klasse mit" schon 
falsch ist. Das folgt schon alleine daraus, dass einigen Aufwand 
bedeuten würde sicherzustellen, dass sich XML-Beschreibung und in C++ 
codierte Klassen nicht widersprechen.
Entweder das C++ Programm ist in der Lage, aus der XML Beschreibung 
intern eine Objektstruktur zu entwickeln (die alle mit nur einer Klasse 
dargestellt werden), oder aber das C++ Programm hat tatsächlich einige 
Klassen für unterschiedliche Objekttypen parat und dafür fallen die 
entsprechenden Beschreibungen aus dem XML raus. Im XML gibt es dann 
'magisch' ein paar Objekttypen, die benutzt werden können.
Beides gemeinsam wird auf lange Sicht für Ärger sorgen (nämlich dann, 
wenn sich XML und C++ über den Objektaufbau widersprechen, was zb leicht 
bei zukünftigen Erweiterungen passieren kann)

von Alexander I. (daedalus)


Lesenswert?

Vielen Dank für die schnellen Antworten. Die Aussage mit dem 
Designfehler habe ich nicht ganz verstanden? Bis auf die Tatsache, dass 
man XML und C-Klassen "konsistent" halten muß, ist es doch nicht 
schlecht?

Es ist noch dazuzusagen, dass ich die abgeleiteten C-Klassen nicht 
großartig ändern sollte, es sind über 450 Stück, die von etwa 10 
verschiedenen Entwicklern über Jahre hinweg geschrieben wurden. Diese 
sind als "gegeben" anzusehen. Eine Änderung an der gemeinsamen 
Basisklasse, oder das hinzufügen einer überladenen Template-Funktion 
wäre besser. Hintergrund ist es, eine Art Schnittstellentester 
(Posix-Funktionen) für die einzelnen Klassen zu erzeugen.

Die Parameter der Klassen können unterschiedlich oder identisch sein und 
dennoch ist die dahinterstehende Funktionalität völlig anders. Eine Art 
Klassenbeschreibungs-String ist doch deshalb zwingend nötig, oder?

Gibt es noch einen anderen Ansatz, wie man in einen Interpreter elegant 
durch "Texteingabe" im weitesten Sinne, Objekte von beliebigen aber 
ihm bekannten C-Klassen erzeugen könnte? Vielleicht sogar ganz ohne XML?

von Karl H. (kbuchegg)


Lesenswert?

Alex Wurst schrieb:
> Vielen Dank für die schnellen Antworten. Die Aussage mit dem
> Designfehler habe ich nicht ganz verstanden? Bis auf die Tatsache, dass
> man XML und C-Klassen "konsistent" halten muß, ist es doch nicht
> schlecht?

Dann warte ab, bis deinen Benutzern es das erste mal passiert, dass die 
beiden Beschreibungen nicht mehr zusammenstimmen.
Viel Spass beim Fehlersuchen :-)

> sind als "gegeben" anzusehen. Eine Änderung an der gemeinsamen
> Basisklasse, oder das hinzufügen einer überladenen Template-Funktion
> wäre besser.

Wenn deine Klassen bereits einen vCCtor haben, hast du gewonnen. Daher 
die Frage (du musst nur in der Basisklasse nachsehen):
Gibt es eine Funktion
1
class Basis
2
{
3
  public:
4
  ...
5
6
  virtual Basis* clone();
7
};

(manche Entwickler nennen die Funktion auch copy())
Funktionalität: Ein Objekt soll eine Kopie von sich selbst erzeugen.

Eine typische Implementierung sieht so aus
1
class Derived1 : public Basis
2
{
3
  public:
4
5
  virtual Derived1* clone() { return new Derived1( *this ); }
6
};
7
8
class Derived2 : public Basis
9
{
10
  public:
11
12
  virtual Derived2* clone() { return new Derived2( *this ); }
13
};

und verwendet werden kann es so:
1
void foo( Basis* pIrgendwas )
2
{
3
  Basis* pCopy = pIrgendwas->clone();
4
}

pCopy ist dann ein Pointer auf eine Kopie von *pIrgendwas. War 
pIrgendwas ein Derived1 Objekt, dann zeigt auch pCopy auf ein Derived1 
Objekt. War *pIrgendwas ein Derived2 Objekt, dann zeigt auch pCopy auf 
ein Derived2 Objekt.

Gibt es keinen derartigen Mechanismus und darfst du die Klassen nicht 
ändern: By,by OOP; welcome switch-case

Alternativ könnte es auch noch geben (ist aber seltener, wenn man das 
nicht von vorne herein hineindesigned)
1
class Basis
2
{
3
  public:
4
  ...
5
6
  static Basis* create();
7
};
8
9
class Derived1 : public Basis
10
{
11
  public:
12
13
  static Basis* create() { return new Derived1; }
14
};
15
16
class Derived2 : public Basis
17
{
18
  public:
19
20
  static Basis* create() { return new Derived2; }
21
};


> ihm bekannten C-Klassen erzeugen könnte? Vielleicht sogar ganz ohne XML?
XML ist nur das Transportmedium. Ob du XML benutzt oder eine ASCII Datei 
oder sonstirgendetwas anderes, löst das Problem nicht.

von Alexander I. (daedalus)


Lesenswert?

Einen virtuellen CopyConstructor gibt es leider nicht.
Eine neue Methode in den abgeleiteten Klassen hinzuzufügen wäre 
prinzipiell schon möglich - sofern einfach realisierbar - aber halt mit 
einigem Anpassungsaufwand verbunden. Die Grundfunktion der Klassen muß 
halt unverändert weiterbestehen.

Die Basisklasse beschreibt folgende Methoden:
1
  IoStatus Open(IoMode);
2
  IoStatus Close(void);
3
  virtual void Init(void) = 0;
4
  virtual void Stop(void) = 0;
5
  virtual IoStatus Read(IoDataRef, IoDataSize);
6
  virtual IoStatus Write(IoDataRef, IoDataSize);
7
  virtual IoStatus Ioctl(IoCmd, IoCmdData);
8
  virtual IoStatus Seek(IoSeekOffset, IoSeekOrg);
9
  virtual IoStatus Flush(void);
10
  virtual IoStatus Sync(void);

Angenommen, eine Änderung in den Klassen wäre kein Problem, wie geht's 
dann weiter? Jede erhält einen CC? Und wie "meldet sich eine Klasse an" 
z.B. in einer verketteten Liste, von der es noch gar kein Objekt gibt? 
Denkfehler meinerseits? Und letzte Frage: Wie handhabe ich es, wenn jede 
Klasse andere Parameter hat die wiederum der Interpreter nicht kennen 
kann?

von Karl H. (kbuchegg)


Lesenswert?

Alex Wurst schrieb:

> Angenommen, eine Änderung in den Klassen wäre kein Problem, wie geht's
> dann weiter? Jede erhält einen CC? Und wie "meldet sich eine Klasse an"
> z.B. in einer verketteten Liste, von der es noch gar kein Objekt gibt?
> Denkfehler meinerseits?

Mal angenommen du gehst den Weg über vCCtor (create Funktion würde 
ähnlich funktionieren)

Dann brauchst du einen Mechanismus, der den Typ (bei dir ein String) mit 
einem Objekt dieses Typs verknüpft.
Da bauen wir doch gleich mal eine Klasse dafür (man könnte auch ein 
std::pair dafür verwenden)
1
class TypeTemplate
2
{
3
public:
4
  TypeTemplate( const std::string& typeString = "", Basis* typeTemplate = NULL )
5
  : typeString_( typeString ),
6
    typeTemplate_( typeTemplate )
7
    {}
8
9
  std::string  typeString_;
10
  Basis*       typeTemplate_;
11
};

eine Factory hält dann zb eine Liste derartiger TypTemplates und eine 
Funktion, die einen Typstring nimmt, ihn sucht und wenn es einen Eintrag 
dafür findet, das typeTemplate auffordert eine Kopie von sich zu 
erzeugen, welches sie dann zurückgibt
1
class TypeFactory
2
{
3
public:
4
  Basis* create( const std::string& type );
5
6
protected:
7
  std::vector< TypeTemplate > types_;
8
};
9
10
Basis* TypeFactory::create( const std::string& type )
11
{
12
  for( size_t i = 0; i < types_.size(); ++i ) {
13
    if( types_[i].typeString_ == type )
14
      return types_[i].typeTemplate_->clone();
15
  }
16
17
  return NULL;
18
}

Bleibt nur noch, dass beim Hochfahren alle Templates registriert werden, 
wofür die Factory gleich mal mit einer register Funktion erweitert wird
1
class TypeFactory
2
{
3
public:
4
  void   register( const std::string& type, Basis* template );
5
  Basis* create( const std::string& type );
6
7
8
protected:
9
  std::vector< TypeTemplate > types;
10
};
11
12
void TypeFactory::register( const std::string& type, Basis* template )
13
{
14
  types_.push_back( TypeTemplate( type, template ) );
15
}
16
17
...
18
19
int main()
20
{
21
  Factory fact;
22
23
  fact.register( "Obj1", new Derived1 );
24
  fact.register( "Obj2", new Derived2 );
25
26
  Basis* newObj = fact.create( "Obj1" );


(Die Factory sollte auch die template Objekte noch im Destructor deleten 
und den op= bzw. ihren eigenen CCtor auf private/nicht implementiert 
setzen. Man könnte auch gleich ein Singleton Pattern für die Factory 
benutzen)

Edit: gerade gesehen. register geht natürlich nicht als Funktionsname. 
Da musst du dir was anderes ausdenken

von Karl H. (kbuchegg)


Lesenswert?

> Und letzte Frage: Wie handhabe ich es, wenn jede
> Klasse andere Parameter hat die wiederum der Interpreter nicht
> kennen kann?

Indem du allen Klassen eine gemeinsame Funktion gibst, die eine 
Argumentliste als Liste übernimmt. Damit haben wieder alle Klassen 
dieselbe Schnittstelle, mit der sie befüllt werden.
Da der Interpreter wie du sagst nicht wissen kann, welche Argumente für 
eine bestimmte Objektklasse gültig sind, muss sich das Objekt selbst aus 
der Argumentliste alles Benötigte extrahieren. Denn nur das Objekt 
selbst weiß, was es alles braucht.

von P. S. (Gast)


Lesenswert?

Lass dich nicht mit Copy-Konstruktoren oder Create-Methoden verwirren. 
Was du willst, ist ganz klassisch eine Factory, die dir die Objekte 
anhand des Namens anlegen kann. Und da du anscheinend alle Klassen zur 
Uebersetzungszeit kennst, machst du das am einfachsten klassisch mit 
einem langen if-elseif-else-Baum der dann eben das jeweilige Objekt mit 
new erzeugt oder schneller in der Laufzeit, aber aufwendiger ueber eine 
Hashtable, in die du kleine Klassenfactories steckst. Mit ersterer 
Variante kannst du auch deine verschiedenen Konstruktorparameter 
hinfummeln, mit letzter wohl eher nicht. Vor Allem versuche nie mit 
Gewalt eine "saubere" OO-Loesung zu bauen, das wird oft nur unleserlich.

von Klaus W. (mfgkw)


Lesenswert?

Alex Wurst schrieb:
> ...
>
> Angenommen, eine Änderung in den Klassen wäre kein Problem, wie geht's
> dann weiter? Jede erhält einen CC?

CC ist hier unpassend, weil du ja kein Objekt zum Kopieren hast.
Vielmehr soll ja eines anhand der XML-Datei erzeugt werden.
Deshalb brauchst du die create()-Methoden, die KHB oben skizziert hat
(im Gegensatz zum KHB-Artikel muß die create() aber static sein!).
Die brauchen dann aber noch einen Parameter, anhand dessen sie
wissen, was zu tun ist; beispielsweise einen Zeiger in die Datei,
ab der die Beschreibung steht oder eine C++-Repräsentation des
XML-Knotens. je nachdem wie du liest.


> Und wie "meldet sich eine Klasse an"

Einmal bei Programmstart oder spätestens bei der ersten Verwendung.
Bei Programmstart geht nicht leicht elegant zu lösen.
Man müsste ein statisches Objekt für jede Klasse anlegen, in dessen
ctor die Anmeldung angetriggert wird.
Das ist insofern kritisch, weil das erst geschehen kann, wenn
bereits die Infrastruktur steht, bei der man anmeldet.
Mit etwas Mühe sollte es aber gehen...

> z.B. in einer verketteten Liste, von der es noch gar kein Objekt gibt?

Liste ist hier ganz schlecht, weil sie bei jedem Suchvorgang
durchlaufen werden muß.
Besser ist eine std::map< std::string, Base*(*)(FILE*) > (bzw.
je nach Typ der create()-Methode der Parameter des zweiten
Arguments angepasst), in der vom Typ auf die zugehörige
create()-Methode abgebildet wird.

> Denkfehler meinerseits? Und letzte Frage: Wie handhabe ich es, wenn jede
> Klasse andere Parameter hat die wiederum der Interpreter nicht kennen
> kann?

Das ergibt sich ganz von selbst, wenn die create() nur einen
Dateizeiger bekommen oder den XML-Knoten wie auch immer und
selbst daraus lesen.
Der Aufrufer braucht gar nicht zu wissen, was da drin steht
und muss auch nichts weiter an Argumenten übergeben.

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:

> CC ist hier unpassend, weil du ja kein Objekt zum Kopieren hast.
> Vielmehr soll ja eines anhand der XML-Datei erzeugt werden.
> Deshalb brauchst du dir create()-Methoden, die KHB oben skizziert hat
> (im Gegensatz zum KHB-Artikel muß die create() aber static sein!).

:-)
Habs schon gesehen :-)

Ich wollte ihn absichtlich nicht in die create-Schiene schicken, damit 
nicht auch noch Funktionspointer ins Spiel kommen. Auf der anderen Seite 
haben solche Monsterframeworks oft schon einen vCCtor eingebaut.

Auf der anderen Seite wäre das wohl die bessere Lösung gewesen.

von Klaus W. (mfgkw)


Lesenswert?

ok, zusammen bekommen wir es schon noch hin :-)

Was ich aber vergessen hatte noch zu erwähnen:
Der Aufrufer liest die Datei soweit, bis er den Typ hat.
Dann geht er mit diesem Typ in die map und ruft die
create()-Methode auf, die er dort findet.

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:
> ok, zusammen bekommen wir es schon noch hin :-)
>
> Was ich aber vergessen hatte noch zu erwähnen:
> Der Aufrufer liest die Datei soweit, bis er den Typ hat.
> Dann geht er mit diesem Typ in die map und ruft die
> create()-Methode auf, die er dort findet.

Jup.
Das erscheint mir ein guter Plan zu sein. Letztenendes verlagert es die 
Funktionalität dorthin, wo sie sein soll: Ins Objekt selber.

Einziges Problem: Sein Monsterframework und die Auflage, dass sich da 
nicht allzuviel ändern darf. Aber bis jetzt sind die Änderungen ja noch 
überschaubar :-)

Wenn allerdings tatsächlich nichts geändert werden darf, dann bleibt nur 
der Weg, das alles extern im Interpreter auf die klassische Art zu 
machen. Nicht schön, nicht besonders wartbar aber dafür fehleranfällig 
:-)

von Klaus W. (mfgkw)


Lesenswert?

.. also etwas für Masochisten.
Die Änderungen sind sogar sehr übersichtlich, weil pro Klasse
eine create-Methode zu schreiben ist. Der Rest findet ausserhalb
statt.

@Alex: wenn du es nicht elegant hinbekommst, kann ich auch morgen
nachmittag ein komplettes Beispiel mit dem ganzen Kram hinschreiben,
das meiste dazu habe ich fertig in der Schublade.
Heute schaffe ich es aber nicht mehr.
Bei Bedarf bitte Bescheid sagen!

von P. S. (Gast)


Lesenswert?

"Es ist noch dazuzusagen, dass ich die abgeleiteten C-Klassen nicht
großartig ändern sollte, es sind über 450 Stück, die von etwa 10
verschiedenen Entwicklern über Jahre hinweg geschrieben wurden."

Ein klares Signal dafuer, nicht in jede Klasse create()-Methoden 
einzubauen. Das Test-Framework sollte den bestehenden Code so wenig wie 
moeglich veraendern.

von Klaus W. (mfgkw)


Lesenswert?

naja, muss er halt selber sehen.
Er hat nach einer eleganteren Möglichkeit gefragt; das scheint ihm
ohne Änderungen nicht elegant genug zu sein.

Den Code, die Parameter zu sammeln und gezielt je nach Klasse
etwas damit zu machen, muß er sowieso schreiben - egal ob in
den 450 Klassen oder außerhalb.

Solchen Code, der eigentlich in die Klassen gehört, außerhalb
unterzubringen und dafür ein switch mit 450 cases, würde ich mal
nicht unbedingt als elegant bezeichnen.

Abgesehen davon, daß die cases gar nicht mit Strings für den Typ
gehen.

von Klaus W. (mfgkw)


Lesenswert?

Alex Wurst schrieb:
> Hallo,
> ...
> Sonstiges:
> Alle Objekte haben eine gemeinsame abstrakte Basisklasse, die
> POSIX-Methoden wie READ, WRITE usw. implementiert.
> ...

Wovon kann READ denn lesen? Aus einer offenen Datei ab der aktuellen 
Stelle?

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:

> Den Code, die Parameter zu sammeln und gezielt je nach Klasse
> etwas damit zu machen, muß er sowieso schreiben - egal ob in
> den 450 Klassen oder außerhalb.

Ich überleg schon die ganze Zeit, ob man nicht mit dem alten Grundsatz 
"Wenn du ein Problem nicht lösen kannst, stülpe eine Klasse drüber" 
weiterkommt.

In etwa so:
Wir möchten eine Klasse um 2 Funktionen erweitern, die aber nicht in der 
Klasse sein sollen. Also leiten wir von der Klasse eine neue Klasse für 
das Testframework ab, welche die beiden fehlenden Funktionen ergänzt.
1
class IODevice : public Basis   // vorhandene Klasse
2
{
3
  ...
4
};
5
6
class TestIODevice : public IODevice
7
{
8
public:
9
  static TestIODevice* create();
10
  void initWithArgs( Arguments );
11
}

Der Testcode arbeitet mit der TestIODevice Klasse, anstelle der IODevice 
Klasse.

Da es hier nur um Erzeugung und Initialisierung geht, könnte man 
allerdings auch so arbeiten:
1
class IODevice : public Basis   // vorhandene Klasse
2
{
3
  ...
4
};
5
6
class TestIODevice
7
{
8
public:
9
  static IODevice* create();
10
  void initWithArgs( IODevice* device, Arguments );
11
}

also quasi eine Klasse, die das unveränderte Objekt erzeugen und 
initialisieren kann.

Kein Grund, da mit riesigen if-else Ketten zu arbeiten. Es gibt immer 
eine OOP Lösung, die wartbarer, besser erweiterbar und übersichtlicher 
ist, als ein if-else-if der über einen Typ läuft.

von P. S. (Gast)


Lesenswert?

Karl heinz Buchegger schrieb:

> Kein Grund, da mit riesigen if-else Ketten zu arbeiten. Es gibt immer
> eine OOP Lösung, die wartbarer, besser erweiterbar und übersichtlicher
> ist, als ein if-else-if der über einen Typ läuft.

Ein Stapel if-else-if ist so leicht lesbar wie eine Tabelle. Und ganz 
sicher nicht schlechter lesbar als der Stapel an Factory- oder 
Template-Instanziierungen, die du sowieso brauchst. Die 
create()-Methoden sind auch die Gleichen, egal ob sie in jeder Klasse 
stehen, in jedem Wrapper oder in der einzelnen Testklasse.

OO ist kein Selbstzweck und es gibt ueberhaupt keine Grund, eine 
OO-Loesung zu produzieren, wenn eine "prozedurale" Loesung so direkt und 
einfach und noch dazu ohne Eingriff in die existierenden Klassen 
moeglich ist.

von Klaus W. (mfgkw)


Lesenswert?

Und es gibt überhaupt keinen Grund, überhaupt OOP zu verwenden,
wenn man es einfach nicht mag :-)

@KHB:
Wenn ich dich richtig verstanden habe, müsste man damit aber
von allen 450 Klassen jeweils eine ableiten?

von P. S. (Gast)


Lesenswert?

Klaus Wachtler schrieb:

> Und es gibt überhaupt keinen Grund, überhaupt OOP zu verwenden,
> wenn man es einfach nicht mag :-)

Mit moegen hat das nichts zu tun.

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:
> Und es gibt überhaupt keinen Grund, überhaupt OOP zu verwenden,
> wenn man es einfach nicht mag :-)
>
> @KHB:
> Wenn ich dich richtig verstanden habe, müsste man damit aber
> von allen 450 Klassen jeweils eine ableiten?

Yep.
Aber da die Klassen alle im Grunde gleich aussehen, kann man da sicher 
mit dem Präprozessor etwas machen (oder Templates, aber mit Templates 
bin ich noch schwach :-)
1
#define WRAPPER(X,Y)                   \
2
class X                                \
3
{                                      \
4
public:                                \
5
  static Y* create() { return new Y; } \
6
  void init(Y* obj, Arguments*)  {}    \
7
}
8
9
// und schon gehts dahin
10
WRAPPER( IODevice, TestIODevice );
11
WRAPPER( UART, TestUART );
12
...

Für die Argument müsste man noch was finden und die Klassenregistrierung 
könnte man sicherlich auch noch ins Makro mit einbringen.

Zur Not gibt es mehrere Makros mit unterschiedlicher Parameteranzahl, 
wobei die zusätzlichen Parameter die Argumenttypen im init regeln.

 Denn besteht der ganze Klassenaufbau im Grunde nur noch aus einer Liste 
dieser Makros mit den entsprechenden Argumenten. Und die create Funktion 
kann das Objekt, dank std::map, rasend schnell erzeugen, bei der 
Registrierung hat man die Gewissheit keine Bezeichnung doppelt verwendet 
zu haben (weil die Registrierfunktion das selbstverständlich überprüft) 
und eine zusätzliche Device Klasse ist einfach nur ein Eintrag mehr in 
dieser Makro-"Tabelle" :-) Herz was willst du mehr!

Man könnte natürlich auch einen Generator schreiben, der diese 
Hilfsklassen aus dem XML-File erzeugt. Fällt mir jetzt gerade ein. Hmm. 
Das wär gar nicht so dumm. Dann hätte man auch gleich die Kontrolle ob 
das XML File einen Fehler enthält. Damit habe ich nämlich immer noch so 
meine Bedenken. Da der TO sowieso XML einliest, muss er auch einen XML 
Parser haben. Und sobald diese Beschreibung im Speicher ist, ist es ja 
ein Klacks, daraus die Klassen zu erzeugen und den Factory-Init Code zu 
generieren.

von Alexander I. (daedalus)


Lesenswert?

Guten Morgen,

hier hat sich ja einiges verselbständigt ;-) Sehr schön. Ich habe 
bereits einen einfachen XML-Parser und zwar TinyXML. Wird bis jetzt 
allerdings nur für die Konfiguration der GUI verwendet und könnte die 
XML-Objekt-Aufgabe übernehmen.

Die READ, WRITE, usw. sind generische I/O-Funktionen von Treibern. 
Dahinter kann sich eine Datei, ein Analogeingang/Ausgang, eine PWM oder 
auch ein UART sowie SD-Kartenleser verbirgen. Das weiß ich alles gar 
nicht - muß ich auch nicht - solange die Schnittstelle so implementiert 
ist wie in der Basisklasse vorgegeben (entspricht teilweise POSIX). Die 
Anzahl Parameter ist (außer beim Konstruktor) auch vorgegeben. Ob sich 
dahinter jetzt aber ein plumper Integer oder ein Zeiger auf eine 
Struktur befindet, wird durch "IoDataRef"-Typedef usw. gekapselt, d.h. 
der Interpreter braucht nicht wissen, was es ist.

Die Lösung von KHB's Post vom 21.09.2009 17:41 erscheint mir bis jetzt 
am praktikabelsten, denn XML möchte ich vermeiden sofern möglich, ist 
aber kein Problem wenn's nötig wäre. Eine Ableitung aller 450 Klassen 
kommt für mich nicht in Frage, dann lieber 1-2 Funktionen in jeder 
Klasse per "intelligentem" Copy+Paste hinzufügen, das dauert dann halt 
mal einen Freitag Nachmittag und eine Rundmail an die Kollegen lang ...

An dem Template-Beispiel hab ich auf jeden Fall Interesse. Vermutlich 
werde ich aber erst morgen wieder zu dem Projekt kommen, deshalb wäre 
heute Nachmittag kein Problem :)

Vielen Dank erstmal wieder für die - wie immer - sehr kompetente Hilfe 
hier!

von Klaus W. (mfgkw)


Lesenswert?

Wenn du kein XML haben willst, mache ich mir das Leben auch einfacher...

Meine Eingabedatei obj.txt sieht dann so aus (je Zeile ein Typ,
dann die Parameter):
1
Objtyp1 25 3.14
2
Objtyp2 125 175
3
Objtyp3 123 456 789
4
Objtyp3 11 22 33
5
Objtyp2 42 21
6
Objtyp1 0 2.718281828

Das Beispielprogramm:
1
// Time-stamp: "22.09.09 22:10 lesevarianten.cpp klaus?wachtler.de"
2
// 
3
// 
4
5
//#include <cstdlib>
6
//#include <cstddef>
7
//#include <cstdio>
8
//#include <cstring>
9
#include <stdexcept>
10
#include <iostream>
11
//#include <iomanip>    // setw(), setfill() etc.
12
#include <fstream>
13
//#include <sstream>    // string streams
14
#include <string>
15
//#include <algorithm>
16
#include <utility>
17
#include <map>  // map<> und multimap<>
18
//#include <set>  // set<> und multiset<>
19
#include <list>
20
//#include <vector>
21
//#include <deque>
22
//#include <>
23
//#include <>
24
//#include <>
25
//#include <>
26
27
28
/////////////////////////////////////////////////////////////////////////////
29
//
30
// Typen fuer die Objekte
31
32
// Basisklasse:
33
class ObjtypBase
34
{
35
public:
36
  // Was halt bisher so alles in der Klasse sein soll...
37
38
39
  ObjtypBase()
40
  {}
41
42
  // immer dabei: virtueller Destruktor:
43
  virtual ~ObjtypBase()
44
  {}
45
46
47
  // um testweise ein Objekt ausgeben zu können, kann sonst auch
48
  // entfernt werden:
49
  virtual void streamout( std::ostream &f ) const = 0;
50
  // Anm: durch das ...=0... wird die Methode pure virtual, d.h. von
51
  // dieser Klasse kann kein Objekt instantiiert werden (nur von
52
  // Ableitungen) -> abstrakte Basisklasse.
53
  // Falls von ObjtypBase doch Objekte geschafft werden sollen, ist
54
  // eine passende Definition von streamout() zu schreiben.
55
56
57
  // Ausserdem:
58
  // factory-Methode, die aus einem Stream ab der aktuellen Stelle ein
59
  // Objekt liest:
60
  static ObjtypBase *create( std::istream &f );
61
};
62
63
// Um so ein Ding ausgeben zu können, kann sonst auch
64
  // entfernt werden:
65
std::ostream &operator<<( std::ostream &stream, const ObjtypBase &o )
66
{
67
  o.streamout( stream );
68
  return stream;
69
}
70
71
// Davon jetzt 450 Ableitungen bilden, die irgendwelche
72
// unterschiedlichen Typen repraesentieren:
73
74
class Objtyp1: public ObjtypBase
75
{
76
public:
77
78
  // Was auch bisher hier so in der Klasse steht...
79
80
81
  // ctor mit den passenden Parametern:
82
  Objtyp1( int i, double d )
83
    : i( i ),
84
      d( d )
85
  {
86
  }
87
88
  // immer dabei: virtueller Destruktor:
89
  virtual ~Objtyp1()
90
  {}
91
92
93
  // um testweise ein Objekt ausgeben zu können, kann sonst auch
94
  // entfernt werden:
95
  virtual void streamout( std::ostream &f ) const
96
  {
97
    f << "Ich bin ein Objtyp1 mit " << i << " " << d;
98
  }
99
100
101
  // Ausserdem:
102
  // factory-Methode, die aus einem Stream ab der aktuellen Stelle ein
103
  // Objekt liest.
104
  // Rückgabe: Zeiger auf das mit new beschaffte Objekt.
105
  static ObjtypBase *create( std::istream &f )
106
  {
107
    int     i;
108
    double  d;
109
110
    f >> i >> d;
111
    return new Objtyp1( i, d );
112
  }
113
114
private:
115
116
  int     i;
117
  double  d;
118
};
119
120
121
122
class Objtyp2: public ObjtypBase
123
{
124
public:
125
126
  // Was auch bisher hier so in der Klasse steht...
127
128
129
  // ctor mit den passenden Parametern:
130
  Objtyp2( int i1, int i2 )
131
    : i1( i1 ),
132
      i2( i2 )
133
  {
134
  }
135
136
  // immer dabei: virtueller Destruktor:
137
  virtual ~Objtyp2()
138
  {}
139
140
141
  // um testweise ein Objekt ausgeben zu können, kann sonst auch
142
  // entfernt werden:
143
  virtual void streamout( std::ostream &f ) const
144
  {
145
    f << "Ich bin ein Objtyp2 mit " << i1 << " " << i2;
146
  }
147
148
149
  // Ausserdem:
150
  // factory-Methode, die aus einem Stream ab der aktuellen Stelle ein
151
  // Objekt liest.
152
  // Rückgabe: Zeiger auf das mit new beschaffte Objekt.
153
  static ObjtypBase *create( std::istream &f )
154
  {
155
    int     i1, i2;
156
157
    f >> i1 >> i2;
158
    return new Objtyp2( i1, i2 );
159
  }
160
161
private:
162
163
  int     i1;
164
  int     i2;
165
};
166
167
168
class Objtyp3: public ObjtypBase
169
{
170
public:
171
172
  // Was auch bisher hier so in der Klasse steht...
173
174
175
  // ctor mit den passenden Parametern:
176
  Objtyp3( int i1, int i2, int i3 )
177
    : i1( i1 ),
178
      i2( i2 ),
179
      i3( i3 )
180
  {
181
  }
182
183
  // immer dabei: virtueller Destruktor:
184
  virtual ~Objtyp3()
185
  {}
186
187
188
  // um testweise ein Objekt ausgeben zu können, kann sonst auch
189
  // entfernt werden:
190
  virtual void streamout( std::ostream &f ) const
191
  {
192
    f << "Ich bin ein Objtyp3 mit " << i1 << " " << i2 << " " << i3;
193
  }
194
195
196
  // Ausserdem:
197
  // factory-Methode, die aus einem Stream ab der aktuellen Stelle ein
198
  // Objekt liest.
199
  // Rückgabe: Zeiger auf das mit new beschaffte Objekt.
200
  static ObjtypBase *create( std::istream &f )
201
  {
202
    int     i1, i2, i3;
203
204
    f >> i1 >> i2 >> i3;
205
    return new Objtyp3( i1, i2, i3 );
206
  }
207
208
private:
209
210
  int     i1;
211
  int     i2;
212
  int     i3;
213
};
214
215
216
// TODO: usw. bis Objtyp450
217
218
//
219
/////////////////////////////////////////////////////////////////////////////
220
221
222
// Beispielverwendung:
223
// Einlesen aus einer Datei und Speichern in einer Liste.
224
// Als Datei wird einfach eine Zeile je Objekt vorausgesetzt;
225
// ggf. anpassen (XML o.ä.).
226
//
227
// In jeder Zeile steht zuerst der Typ, dann die Parameter zum
228
// Initialisieren (je nach Typ unterschiedlich).
229
230
231
int main( int nargs, char **args )
232
{
233
  // Eine Map bauen, die für jeden Typ einen Zeiger auf die zugehörige
234
  // factory-Methode speichert:
235
  std::map< std::string, ObjtypBase *(*)( std::istream & ) >  mapTypCreate;
236
237
  mapTypCreate["Objtyp1"] = Objtyp1::create;
238
  mapTypCreate["Objtyp2"] = Objtyp2::create;
239
  mapTypCreate["Objtyp3"] = Objtyp3::create;
240
  // TODO: usw. bis Objtyp450
241
242
243
244
  // Liste, um testweise alle Objekte zu lesen und in Liste speichern:
245
  std::list< ObjtypBase * >   listObj;
246
247
  {
248
    std::ifstream     f( "obj.txt" );
249
    std::string       typ;
250
251
    // Zeilenweise je einen Typ lesen, Stream steht dann vor den Parametern
252
    while( (f >> typ) )
253
    {
254
      if( typ!="" )
255
      {
256
        if( mapTypCreate.find( typ )!=mapTypCreate.end() )
257
        {
258
          // mit den restlichen Parametern ein Objekt bauen lassen und
259
          // in die Liste haengen:
260
          listObj.push_back( mapTypCreate[typ]( f ) );
261
        }
262
        else
263
        {
264
          std::cerr << "Fehler: Typ " << typ << " unbekannt" << std::endl;
265
        }
266
      }
267
268
    }
269
  }
270
271
272
  // zur Kontrolle die Liste ausgeben:
273
274
  for( std::list< ObjtypBase * >::const_iterator itListe=listObj.begin();
275
       itListe!=listObj.end();
276
       ++itListe
277
       )
278
  {
279
    std::cout << "Listenelement: " << (**itListe) << std::endl;
280
  }
281
282
  return 0;
283
}

ich habe jetzt nur die ersten 3 von 450 geschrieben...

Jeder Typ kann eigene Typen und Anzahl der Parameter haben.
Wenn die Datei nicht so aussieht, sind entsprechend die
create()-Funktionen anzupassen.
Falls sie gar nicht aus einer Datei lesen, sondern z.B.
aus einer XML-Repräsentation, entsprechend auch ihre
Parameterlisten und passend dazu der zweite Typ der Map
(Zeiger auf die create()).

Kompilieren und ausführen sieht so aus:
1
klaus@i4a:~ > g++ -Wall lesevarianten.cpp && ./a.out 
2
Listenelement: Ich bin ein Objtyp1 mit 25 3.14
3
Listenelement: Ich bin ein Objtyp2 mit 125 175
4
Listenelement: Ich bin ein Objtyp3 mit 123 456 789
5
Listenelement: Ich bin ein Objtyp3 mit 11 22 33
6
Listenelement: Ich bin ein Objtyp2 mit 42 21
7
Listenelement: Ich bin ein Objtyp1 mit 0 2.71828

von Alexander I. (daedalus)


Lesenswert?

Hallo nochmal,

ich glaube das Prinzip habe ich jetzt verstanden. Werde gleich mal mein 
Visual Studio fragen, was es dazu meint. Danke soweit erstmal!

von Klaus W. (mfgkw)


Lesenswert?

Viel Spaß damit!
Was ich natürlich komplett unterschlagen habe ist jedwede 
Fehlerbehandlung.
Z.B. können die create() feststellen, daß ihre Parameter nicht passen 
und
müssten dann eine Ausnahme werfen o.ä.; aber das ist ein anderes Thema.

von Alexander I. (daedalus)


Lesenswert?

Hallo,

also rund 8 Stunden später bin ich ein ganzes Stück weiter! Ich kann von 
meinen Klassen beliebig viele Objekte anlegen, z.B. so:
1
// Registrierung
2
ChannelTypeMap["obj"] = AbgeleiteteKlasse1::Create;
3
4
// Instanzierung
5
ChannelMap["instanz1"] = ChannelTypeMap["obj"]("instanz1", 1, 2, 3);
6
7
// Verwendung
8
ChannelMap["instanz1"].Write(&data, sizeof(data));

Es funktioniert eigentlich genau so wie ich das haben wollte. Hatte noch 
versucht die Registrierung irgendwie zu automatisieren, aber nach einer 
Stunde Fehlversuchen hab ich das dann gelassen. So dürfte das okay sein, 
es ist eine Collage aus KHB's und Ihrem Code.

Ich habe das create() noch etwas "aufgebohrt" in dem es eine va_list 
akzeptiert. Dem Interpreter ist es also völlig egal wieviel Parameter 
von welchem Typ, der übergibt das halt. Jetzt muß halt jede abgeleitete 
Klasse das statische create() implementieren um mitspielen zu dürfen, 
ansonsten ist sie zwar verwendbar aber mit dem Interpreter nicht 
ansprechbar.

Vielen Dank nochmal, hat mir sehr geholfen!

von Karl H. (kbuchegg)


Lesenswert?

Alex Wurst schrieb:

> Vielen Dank nochmal, hat mir sehr geholfen!

Ich denke das wichtigste was du mitnehmen sollst, ist die Technik die 
dahintersteckt. In einer OOP Welt nenn man so etwas ein 
'Design-Pattern'. Klar hätte man auch eine ordinäre if-else-if machen 
können. OOP kann nichts, was man nicht mit den althergebrachten Mitteln 
auch erreichen könnte. Aber oftmals sind die OOP Mitteln universeller.
Wenn du zb in Zukunft ein Programm brauchst, das über die Fähigkeit 
verfügt zur Laufzeit zusätzliche Objekte einzubinden (auch dann wenn sie 
in einer DLL stecken), dann kennst du jetzt eine OOP Methodik mit der 
man das 'simpel' machen und organisieren kann: Objekte, die sich bei 
einer Factory registrieren. Wie das dann konkret aussieht (templatre 
Objekt, create Funktion, ...) ist nicht der springende Punkt. Der 
springende Punkt ist, dass es eine zentrale Stelle gibt, die über eine 
standarddisierte und dem Objekt vorgeschrieben Schnittstelle ein Objekt 
erzeugen kann.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.