Hallo!
Ich überlege gerade die grundsätzliche Programmstruktur (Klassenstruktur
etc. ) für mein Programm.
Ws geht darum, dass ich verschiedene Netzwerknachrichten (als Bytestrom
) bekomme und diese gerne komfortabel in meinem Programm
weiterverarbeiten möchte. Ich suche dafür eine elegante C++ Lösung.
In C würde ich ein Datentyp definieren der als Union für jeden
Nachrichtentyp ein Struct enhält. So dass ich bequem auf die Nutzdaten
jedes Nachrichtentyps zugreifen kann.
Wie würde man das elegant in C++ lösen? Oder würde man das genau so
machen?
Ja zur Not. Aber normalerweise hält C++ ja für fast alle C-Konzepte eine
alternative mit gewissen Vorteilen bereit ;)
Und da ich in C++ Programmiere würde ich ungerne in C-Syle zurück
fallen, wenn es eine elegantere alternative gibt.
Wenn man das mal von einer etwas abstrakteren Ebene aus betrachtet, so
sind structs die Beschreibung von Datenstrukturen im Speicher und unions
das Mittel um mehrere solcher Beschreibungen sozusagen simultan auf
einen einzigen Speicherbereich zu legen.
In C++ gibt es genauso structs und unions wie in C. Hingegen gibt es
kein anderes Sprachmittel, das genau dieses leistet und es wäre auch
redundant ein weiteres vorzuhalten (wenn es nicht weitere Möglichkeiten
bieten würde).
Würde sich jedoch aus irgendwelchen Gründen der Zwang ergeben auf
structs und unions zu verzichten, bliebe noch, nach genauem Studium des
Layouts von instanziierten Individuen einer Klasse, der Weg, mehrere
Klassen mit unterschiedlichen Mitgliedsvariablen zu erzeugen und Zeiger
auf diese zu verwenden, die alle auf den selben Speicherbereich zeigen.
Weiter, bliebe der, mehr traditionelle, Weg, den Datenstrom beim Eingang
zu parsen und die Felder entsprechend zu verarbeiten.
Es bliebe zu berücksichtigen, das sich die Mitglieder von structs mit
einem pragma packen liessen, was nicht eigentlich C sondern eine
spezielle Compilereigenschaft wäre (und bei den mir bekannten
tatsächlich ist). Ob es eine analoge Compileranweisung für Mitglieder
von C++-Klassen gibt ist mir nicht bekannt.
Im allgemeinen ist die einzige "natürliche" Oberklasse des Inhalts eines
ansonsten sehr heterogenen Datenstrom eben der Datenstrom. Aber sogar
eine sehr grosse Ähnlichkeit der Datensätze würde zwar die Bildung von
mehreren miteinander verwandten Klassen nahelegen; dennoch für eine
Implementierung nicht zwangsweise bedeuten, das hier Speicherbereiche
beschrieben werden.
Wahrscheinlich würde man vielmehr (abgesehen von identischen linken
Seiten) unterschiedliche Parser für den verbleibenden Datenstrom
implementieren.
Eine andere Alternative wäre die Definition von Containern die viele
kleine Klassen für jeweils ein Element des Datenstroms enthalten, die
jeweils ihren eigenen Teilabschnitt des Datenstroms parsen.
Mein Fazit ist, das es keine "elegantere" Möglichkeit gibt wenn die
Grundidee beibehalten werden soll. Wobei zu fragen bliebe, was den
"Eleganz" in Deinen Augen ist. (Womit ich meine, das es eine objektive
Beschreibung dieses Kriteriums geben muss, damit man ein entsprechendes
Konstrukt wählen kann).
Der Vorteil von struct/unions ist, das man nur jeweils einmal die
Struktur beschreibt und bei Änderungen der Reihenfolge etc. nur die
Struktur ändert, jedoch an der Verarbeitung nichts ändern muss. Werden
aber Parser implementiert muss man, da der Ablauf die Grammatik
beschreibt, sehr aufpassen und bei Änderungen noch viel mehr.
Das hängt natürlich auch irgendwie davon ab, wie man die
verschiedenen Nachrichten unterscheiden kann.
Wenn es noch nicht anderweitig festgelegt ist und man noch Einfluß
darauf hat, kann man das ja nutzen.
Ich würde vermutlich eine Nachricht so aufbauen, daß am Anfang zur
Sicherheit eine bestimmte Kennung kommt (um absoluten Unfug
blocken zu können), dann bei Bedarf eine Längenangabe für den Rest
der Nachricht (dadurch kann der Empfänger ggf. seinen
Empfangspuffer anpassen und den Rest in einem Rutsch lesen).
Anschließend soll dann eine Kennung kommen, die den Typ der
Nachricht exakt definiert.
Für jede Art von Nachricht baut man sich eine entsprechende
Klasse, die sinnvollerweise von einer passenden Basisklasse
abgeleitet (z.B. ABC_Message) ist. Wahrscheinlich wird man von
dieser Basisklasse nie ein Objekt schaffen, also virtuell machen
(ABC = abstract base class).
Eine Methode dieser Klasse (factory method, virtueller
Konstruktor, z.B. create()) wird von den Ableitungen
jeweils überschrieben, um ein Objekt des richtigen Typs zu
erzeugen.
Eine weitere statische Methode der Basisklasse dient dazu, anhand
einer eindeutigen ID jeder abgeleiteten Klasse (Klasse, nicht
Objekt!) ein zugehöriges Muster der abgeleiteten Klasse speichern
zu können. Ich nenne sie mal register_prototyp(), weil sie einen
Prototypen registriert.
Für jede abgeleitete Klasse muß ein solches Muster (ein
Prototyp) bei der Basisklasse angemeldet werden.
Das kann durch ein statisches Element bei Programmstart passieren;
dann muß man aber aufpassen, dass die Reihenfolge aller statischen
Elemente stimmt.
Es ist auch denkbar, alles was zu einer solchen abgeleiteten
Klasse gehört, in eine DLL auszulagern (die in ihrer
Initialisierungsfunktion dann das Anmelden übernimmt). Dann kann
ein bereits kompiliertes (!) Programm beim Kunden ohne neues
Übersetzen und Linken um neue Nachrichtentypen erweitert werden,
indem man passende DLLs ergänzt (falls die über Namen, z.B. aus
Konfigurationsdateien geladen werden können).
Ab dann kann eine weitere statische Methode der Basisklasse anhand
einer Klassen-ID über das zugehörige Musterobjekt die
create()-Methode der richtigen Klasse aufrufen, die dann den
Bytestrom bekommt und daraus ein ordentliches Objekt baut.
Ich habe das mal zu einem möglichst kleinen vollständigen Beispiel
zusammengeschraubt...
Es geht dabei um die abstrakte Basisklasse ABC_Message und zwei
davon abgeleitete Nachrichtentypen (die sich nicht so richtig
unterscheiden, aber egal): MessageTyp1 und MessageTyp2.
Erst das Testprogramm, das ist noch am übersichtlichsten:
=ABC_Message::createMessageObject("Typ1\nHallo, ich bin der erste");
35
ABC_Message*pZweiteNachricht
36
=ABC_Message::createMessageObject("Typ2\nHallo, ich bin der zweite");
37
38
// Verwenden:
39
if(pErsteNachricht)
40
{
41
pErsteNachricht->zeigdich();
42
}
43
if(pZweiteNachricht)
44
{
45
pZweiteNachricht->zeigdich();
46
}
47
}
48
catch(std::exception&Fehler)
49
{
50
std::cerr<<"Fehler: <"
51
<<Fehler.what()
52
<<"> in "
53
<<__PRETTY_FUNCTION__
54
<<"\n";
55
}
56
catch(...)
57
{
58
std::cerr<<"unbekannter Fehler in "
59
<<__PRETTY_FUNCTION__
60
<<"\n";
61
}
62
}
Zuerst werden mit einer statischen Methode
ABC_Message::register_prototyp() zwei Klassen "angemeldet",
nämlich MessageTyp1 und MessageTyp2 mit je einem Namen. Dieser
Name ist der, der dann auch später im Datenstrom als Kennung
auftauchen soll, anhand dessen die Objekte erzeugt werden sollen.
Dann werden zwei solche Objekte erzeugt, indem ein passender
String an ABC_Message::createMessageObject() übergeben wird. Ich
habe das jetzt so festgelegt, daß in dem String erst die Id kommen
muss, dann ein Zeilenvorschub und dann die restlichen Daten. Mit
diesen restlichen Daten hat ABC_Message nichts zu tun, sondern
nimmt anhand der Id den Prototypen vom passenden Typ, der beim
Anmelden übergeben wurde, und ruft darüber den zugehörigen
virtuellen Konstruktor der passenden Klasse auf, dem werden dann
die Daten nach dem LF übergeben und er muß dann irgendwie daraus
das Objekt bauen.
Macht natürlich nur Sinn, wenn er dann wiederum mit den
Initialisierungsdaten nach dem LF etwas sinnvolles anfangen kann.
Daraus bekommt man je einen Zeiger auf ein neues Objekt, und kann
es irgendwie verwenden.
Als Beispiel habe ich einfach eine Methode zeigdich() eingebaut;
im echten Leben würde da vermutlich mehr kommen.
Die Basisklasse ABC_Message enthält eine statische Map, in der die
Zuordnung zwischen den IDs und den Prototypen abgelegt ist, sowie
die Methoden zum Registrieren der Klassen und Erzeugen der
Nachrichtenobjekte:
Die beim Anmelden erzeugten Prototypen werden in dieser Form
übrigens erst bei Programmende gelöscht. Dazu muss der Destruktor
virtuell sein, weil in der Map ja eigentlich nur ABC_Message*
abgelegt sind, in Wirklichkeit aber davon abgeleitete Objekte
zerstört werden müssen!
Weiterhin funktioniert das so nur, wenn es keine Kollisionen durch
Threads gibt.
Falls das zu befürchten ist, muß man natürlich die Zugriffe auf
die Map mit Semaphoren irgendwie abdichten.
Die eigentlichen Nachrichten können natürlich auch indirekt von
ABC_Message abgeleitet sein, so daß ganze Hierarchien gebaut werden
können.
Weil die Klasse noch ein static Element enthält (die Map), muß
diese in einer CPP-Datei definiert werden:
// TODO: Daten korrekt verwenden (parsen etc.), um Objekt sinnvoll
37
// zu bauen!
38
returnnewMessageTyp2(szContent);
39
}
40
41
voidzeigdich()
42
{
43
std::cout<<"ich bin ein MessageTyp2: "<<sWerbinich<<std::endl;
44
}
45
46
private:
47
48
std::stringsWerbinich;
49
50
};// class MessageTyp2
51
52
#endif /* ifndef __Message_Typ2_h__ */
53
54
// Local Variables:
55
// mode:C++
56
// End:
Nur aus Faulheit habe ich eine zusätzliche Headerdatei verwendet,
die eigentlich hier nicht hingehört, aber zum Übersetzen der
obigen Beispiele nötig ist:
ich bin ein MessageTyp2: Hallo, ich bin der zweite
Mit dem MS-Compiler habe ich es nicht getestet. Im Prinzip sollte
es gehen, aber ich glaube, der kommt mit den throw-Deklarationen
nicht klar. Im Zweifelsfall einfach weglassen.
Sowohl Sender als auch Empfänger im Netzwerk können intern bei
Bedarf mit denselben Nachrichtentypen arbeiten, müssen es aber
nicht.
Noch als Anmerkung zur ursprünglichen Frage:
Das Ersatzkonzept für die union ist in C++ hier die Ableitung von
einer Basisklasse unter Einsatz virtueller Methoden.
Das funktioniert so, daß man einen Zeiger haben kann, der formal
auf ein Objekt der Basisklasse zeigt, aber auch auf ein Objekt
einer beliebigen davon abgeleiteten Klasse zeigen darf.
Ruft man über einen solchen Zeiger eine Methode auf, dann gibt
es zwei Möglichkeiten:
- die Methode ist nicht als virtual deklariert, dann wird für
das abgeleitete Objekt leider die Methode der Basisklasse aufgerufen
- die Methode ist virtual, dann wird auch tatsächlich die Methode
aus der abgeleiteten Klasse verwendet, die zu dem Objekt gehört.
Mit virtual braucht der Aufrufer also gar nicht die abgeleitete
Klasse kennen, kann aber trotzdem über einen Zeiger eine Methode
daraus aufrufen.
Indem man also Zeiger auf verschiedene Datentypen hat, wird erst
zur Laufzeit die Methode ausgesucht, die wirklich zu dem
betreffenden Objekt gehört.
Das ist deutlich eleganter als eine union.
(Mit Referenzen statt Zeigern analog...)
Klaus Wachtler schrieb:
> ...> Für jede Art von Nachricht baut man sich eine entsprechende> Klasse, die sinnvollerweise von einer passenden Basisklasse> abgeleitet (z.B. ABC_Message) ist. Wahrscheinlich wird man von> dieser Basisklasse nie ein Objekt schaffen, also virtuell machen> (ABC = abstract base class).> ...
Sorry, das soll nicht heißen "virtuell machen" sondern "abstrakt
machen".