Filestream write() - Serialisierung von C++-Klassen
-
Der Compiler weiß das nicht. sizeof liefert die Größe des Typs, .write() iteriert mit einer Schleife darüber und schreibt Byte für Byte.
-
Katzenzunge_n schrieb:
Object ist ja eine beliebige Klasse.
Ja, und die Daten einer Instanz stehen auch irgendwo im Speicher. Auf diesen Speicherbereich besorgt man sich einen Zeiger und schreibt den Inhalt in die Datei.
Und was liefert eigentlich sizeof(Object)?
Das liefert die Größe inklusive des Paddings. Daher sollte man das auch normalerweise nicht mit beliebigen Klassen/Strukturen so machen.
Die Member von Strukturen (wie geschrieben nur PODs), die in einem Rutsch in den Speicher geschrieben/ aus ihm geladen werden sollen, sollten mit Compilerschaltern auf 1 Byte ausgerichtet sein.
-
314159265358979 schrieb:
Der Compiler weiß das nicht. sizeof liefert die Größe des Typs, .write() iteriert mit einer Schleife darüber und schreibt Byte für Byte.
Inwiefern iteriert write() in einer Schleife?
yahendrik schrieb:
Ja, und die Daten einer Instanz stehen auch irgendwo im Speicher. Auf diesen Speicherbereich besorgt man sich einen Zeiger und schreibt den Inhalt in die Datei.
Eben. Aber woher weiß der Compiler welche Daten der Klasse er wie in die Datei schreiben soll? Selbst bei POD muss es doch ein festes Schema geben, nach der die Daten geschrieben werden. Man nehme an ich habe zwei int und ein double
class { public: int a; double b; int c;
Speichert der Compiler die Daten in der Reihenfolge, wie ich diese in der Klasse definiert habe oder wie?
Offensichtlich wird hier die Klasse einfach als alter C-Struct betrachtet.
yahendrik schrieb:
Das liefert die Größe inklusive des Paddings. Daher sollte man das auch normalerweise nicht mit beliebigen Klassen/Strukturen so machen.
Die Member von Strukturen (wie geschrieben nur PODs), die in einem Rutsch in den Speicher geschrieben/ aus ihm geladen werden sollen, sollten mit Compilerschaltern auf 1 Byte ausgerichtet sein.Das heißt, sizeof liefert mir die Größe der Datenfelder in der Klasse? Das wären oben dann 16 Byte?
Was konkret bedeutet Padding hier? Padding kenne ich als Begriff nur aus der Kryptographie, wo es um das Auffüllen von Bytes geht.
Ist überhaupt vom C++-Standard gewährleistet, in welcher Byte-Reihenfolge (Byte-Order oder Endianness) geschrieben wird oder bestimmt das die Zielplattform bzw. der Compiler?
-
Katzenzunge_n schrieb:
Meine Frage ist im Prinzip, woher der Compiler weiß was er da überhaupt in welcher Reihenfolge in die Datei schreiben kann? Object ist ja eine beliebige Klasse.
Es wird einfach plump alles was an der Stelle steht in der Reihenfolge in der es da steht in die Datei geschrieben und zwar so als ob es chars wären.
Und was liefert eigentlich sizeof(Object)?
"sizeof" ist englisch für "Größe von". Die Größenangabe ist in Einheiten von chars.
Probieren wir's einfach mal aus:
#include<iostream> #include<string> using namespace std; struct Foo { char a[4]; int b; short c; short d; short e; string f; }; int main() { Foo foo={{'h','u','h','u'},12345,97,98,99,"Hallo Welt"}; cout.write(reinterpret_cast<char*>(&foo), sizeof(Foo)); cout << endl; }
Hier passiert allerlei undefiniertes Zeug, daher kann es sein, dass deine Ausgabe anders aussieht als meine, aber sie sollte zumindest so oder ähnlich sein:
huhu90abc(PS
Es ist festgelegt, dass die Member im Struct in der Reihenfolge gespeichert werden, in der sie deklariert worden sind. Es kann aber noch Füllzeichen dazwischen geben. Mein Compiler scheint hier keine eingeführt zu haben.
Daraus folgt, dass am Anfang auf jeden Fall das "huhu" kommen muss, welches schon als chars vorlag und so eingespeichert wurde.
Die "90" danach ist die Binärdarstellung von 12345 auf meinem System, als chars interpretiert. In einer ASCII-Tabelle schlägt man einfach nach, dass '9' = 57 und '0' = 48. Und tatsächlich ist 48*256 + 57 = 12345, was uns etwas darüber verrät, wie auf meiner Maschine Integerwerte gespeichert werden. (Ein aufmerksamer Leser wird nun bemerken, dass nur zwei Zeichen ausgegeben wurden. Ist sizeof(int) bei mir 2? Nein, die anderen chars haben bloß alle den Wert 0 und das Nullzeichen ist in der Ausgabe nicht sichtbar)
Danach folgen die shorts. Anscheinen hat mein Compiler wieder keine Füllzeichen eingebaut oder die Füllzeichen sind alle 0. Dieses Mal ist die ASCII-Tabelle umgekehrt zu benutzen. Ich habe die Werte 97,98,99 so gewählt, dass sie "abc" ergeben, was ebenfalls passt.
Als letztes kommt der String und er zeigt, warum das Verfahren bei nicht-POD überhaupt nicht funktioniert. Das "Hallo Welt" hat der String irgendwo im Freispeicher angelegt, was man hier sieht sind die internen Datenstrukturen des Strings die auf das Hallo-Welt verweisen, vermutlich ein paar Pointer. Diese haben irgendwelche Werte und werden von meiner Konsole als irgendwelche wilden Unicodezeichen interpretiert, die hier im Forum nicht darstellbar sind.
edit: Mittlerweile sind ein paar Fragen dazu gekommen:
Das heißt, sizeof liefert mir die Größe der Datenfelder in der Klasse? Das wären oben dann 16 Byte?
Nein, es wird alles zusammengezählt, inklusive eventueller Füllzeichen.
Was konkret bedeutet Padding hier? Padding kenne ich als Begriff nur aus der Kryptographie, wo es um das Auffüllen von Bytes geht.
Das sind Füllzeichen die der Compiler aus Optimierungsgründen einfügen darf. Aus gewissen technischen Gründen ist es nämlich günstiger, die Daten an bestimmten Vielfachen (meistens von 4 oder
zu orientieren. Wenn der erste Member nur 3 Byte groß ist, ist es daher günstig, vor dem zweiten ein Byte frei zu lassen.
Ist überhaupt vom C++-Standard gewährleistet, in welcher Byte-Reihenfolge (Byte-Order oder Endianness) geschrieben wird oder bestimmt das die Zielplattform bzw. der Compiler?
Nein, das ist überhaupt gar nichts garantiert. Und das ist ein großes Problem bei der Serialisierung. Daher macht man so etwas auch nicht mit write, sondern gibt alles hübsch als menschenlesbares ASCII (das verstehen (fast) alle Computer) aus oder benutzt für sein eigenes Programm eine Serialisierungsbibliothek, welche ein plattformunabhängiges Speicherschema benutzt.
-
Ok, danke für die Aufklärung, besonders an SeppJ.
Habe zum Thema Padding hier noch zwei hilfreiche Quellen gefunden.
http://msdn.microsoft.com/en-us/library/71kf49f1(v=vs.80).aspx
http://en.wikipedia.org/wiki/Data_structure_alignment#Data_structure_padding
-
Katzenzunge_n schrieb:
Inwiefern iteriert write() in einer Schleife?
void write(char* data, int size) { while(size--) write_char(*data++); }
So in etwa _könnte_ write funktionieren. Muss es aber nicht. Nirgens steht geschrieben, ob write nicht auch z.B. memmove verwenden könnte. Aber man kann sich das in etwa so vorstellen.
-
Was genau write intern verwendet, dürfte auch davon abhängen, welchen Stream du angegeben hast. Es dürfte wohl die streambuf::sputn() für die tatsächliche Ausgabe bemühen. Wenn das nicht für den speziellen Stream-Typ optimiert wurde, läuft diese wiederum auf eine Schleife für die byteweise Ausgabe hinaus.
(die Optimierungen können dann in einem direkten Aufruf von memcpy(), fwrite() oder std::string::append() bestehen - je nachdem, worüber du redest)
-
ofstream::write nutzt doch unter der Haube fwrite() ?
Zumindestens in der MSVC STl Implementierung.
-
314159265358979 schrieb:
Katzenzunge_n schrieb:
Inwiefern iteriert write() in einer Schleife?
void write(char* data, int size) { while(size--) write_char(*data++); }
So in etwa _könnte_ write funktionieren. Muss es aber nicht. Nirgens steht geschrieben, ob write nicht auch z.B. memmove verwenden könnte. Aber man kann sich das in etwa so vorstellen.
Hi!
Das ist mir schon klar.
Es ging dabei mehr um die Frage, wie write() über die Datenmember iteriert? SeppJ hat ja bereits erklärt das beim Casten einfach aus dem Speicher sequentiell geschrieben wird und er hat auch erklärt wie diese in einem Struct abgebildet werden.
Es ging also nicht darum wie write() funktioniert, sondern was von write() überhaupt beim Casten einer Klasseninstanz geschrieben wird.
-
Ethon schrieb:
ofstream::write nutzt doch unter der Haube fwrite() ?
Zumindestens in der MSVC STl Implementierung.Ist absolut erlaubt, aber vom Standard nicht gearantiert - genausogut könnte es hinter den Kulissen mit man: write(2) arbeiten oder mit einer speziell dafür vom Compiler-Hersteller geschriebenen Bibliothek.
-
Padding ist durchaus interessant.
#include <iostream> using namespace std; class A { public: char a; char b; int c; }; class B { public: char a; int c; char b; }; int main(void) { int sizeA = sizeof(A); int sizeB = sizeof(B); cout << "A = " << sizeA << endl; cout << "B = " << sizeB << endl; return 0; }
Mein VC++ liefert mir hier die Ausgabe:
A = 8 B = 12
sizeof(9 auf einer Klasse aufzurufen scheint wirklich ziemlich abenteuerlich zu sein.
-
Und jetzt füge Mal virtuelle Funktionen hinzu...
~Ich nehme an, dir ist klar, dass struct das selbe ist wie class, nur mit einem standardmäßigem public-Sichtbarkeitsbereich~
-
Oberon_0 schrieb:
Ich nehme an, dir ist klar, dass struct das selbe ist wie class, nur mit einem standardmäßigem public-Sichtbarkeitsbereich
Ist mir bekannt.
Oberon_0 schrieb:
Und jetzt füge Mal virtuelle Funktionen hinzu...
Interessant. sizeof() liefert mit einer virtielle Funktion nun 16, statt 12 zurück. Dürfte wohl der zusätzlich Pointer (0) sein?
-
Interessant. sizeof() liefert mit einer virtielle Funktion nun 16, statt 12 zurück. Dürfte wohl der zusätzlich Pointer (0) sein?
Ja, der Pointer zur Functiontable.
Und jetzt erbe die Klasse, und füg neue virtuelle Funktionen dazu.Aber ernsthaft: Sachen in eine POD-Representation bringen und direkt schreiben/lesen ist imo keine so schlechte Idee, man sollte halt compilerunabhängig Padding/Alignment festsetzen und auf die Endianess aufpassen.
-
Ethon schrieb:
Aber ernsthaft: Sachen in eine POD-Representation bringen und direkt schreiben/lesen ist imo keine so schlechte Idee, man sollte halt compilerunabhängig Padding/Alignment festsetzen und auf die Endianess aufpassen.
Meiner Ansicht nach ist davon eher strikt abzuraten, zumindest wenn man in ANSII C++ programmiert.
Dann lieber Boost verwenden oder den operator<< überladen und die Daten selbst im festen Format schreiben.
Alles andere sieht mir angesichts der Sachlage nach ziemlichen Murks aus.
-
Katzenzunge_n schrieb:
Ethon schrieb:
Aber ernsthaft: Sachen in eine POD-Representation bringen und direkt schreiben/lesen ist imo keine so schlechte Idee, man sollte halt compilerunabhängig Padding/Alignment festsetzen und auf die Endianess aufpassen.
Meiner Ansicht nach ist davon eher strikt abzuraten, zumindest wenn man in ANSII C++ programmiert.
Dann lieber Boost verwenden oder den operator<< überladen und die Daten selbst im festen Format schreiben.
Alles andere sieht mir angesichts der Sachlage nach ziemlichen Murks aus.
Die meisten Fileformate sind allerdings binär. Was auch einen Grund hat.
Geringerer Platzbedarf und man muss nicht parsen.Wer möchte denn 100MB große Mp3 Dateien haben, die mehrere Sekunden laden?
-
Nur sind solche Formate dann standardisiert und hängen nicht von Compiler und Plattform ab. Natürlich machen Binärformate Sinn, aber sobald du sowas portabel hinkriegen willst, musst du eh memberweise vorgehen. Zumal in C++ der Grossteil der Klassen keine PODs sind.
Du kannst das Ganze auch abstrahieren, wie das Boost.Serialization mit seinen Archiven macht. Auf der einen Seite schreibst und liest du die einzelnen Member, auf der anderen Seite hast du ein Archiv, welches die aufgerufenen Schreib-/Lesefunktionen umsetzt. Diese Umsetzung bestimmt dann das eigentliche Format. Das kann sowohl Text als auch binäre Daten sein.