Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

propper

Frischling

  • »propper« ist der Autor dieses Themas

Beiträge: 21

Wohnort: Internet

  • Private Nachricht senden

1

15.03.2015, 03:12

[C++] Abgeleitete Klassen in Binärdatei schreiben und lesen

Hallo ihr Menschen,
wie die meisten Threadersteller habe auch ich ein Problem:
Ich habe eine Basisklasse namens Block, hier mal auf das wichtigste reduziert. Sie hat drei virtuelle Funktionen, die in den zahlreichen abgeleiteten Klassen so wie üblich überschrieben werden. Von den abgeleiteten Klassen habe ich auch mal eine kleine Auswahl (zwei, da sich lediglich der Name und der Funktionsinhalt ändert) weiter unten bereitgestellt. Doch zunächst die Basisklasse:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
class Block
{
public:
    Block() {}; //Konstruktor
    ~Block() {}; //Destruktor

    virtual void HandleEvents() {};
    virtual void Update(float NewFrameTime) {};
    virtual void Render() {};

    inline void SetMainData(sf::Texture *NewBlockTexture) { ... }
};

Ableitung Block_Air:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
class Block_Air : public Block
{
public:
    Block_Air();
    Block_Air(const Block &B) : Block(B) {};
    ~Block_Air() {};

    void HandleEvents();
    void Update(float NewFrameTime);
    void Render();
};

Ableitung Block_Dirt:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
class Block_Dirt : public Block
{
public:
    Block_Dirt();
    Block_Dirt(const Block &B) : Block(B) {};
    ~Block_Dirt() {};

    void HandleEvents();
    void Update(float NewFrameTime);
    void Render();
};

[hier weitere Ableitungen hindenken...]


Nun möchte ich ein Objekt einer Abgeleiteten Klasse in eine Binärdatei speichern (Bsp. an Block_Air). Ich habe einen Pointer (Typ Block) zu diesem Objekt (Bereitgestellt durch ChunkToSave->GetBlock(x, y)). Das Speichern funktioniert immer bestens:

C-/C++-Quelltext

1
2
3
std::ofstream Write(FilePath, std::ios::binary);
Write.write((char*)ChunkToSave->GetBlock(x, y), sizeof(Block)); //"ChunkToSave->GetBlock(x, y)" gibt einen Pointer zu der zu speichernden Klasse Block zurück
Write.close();


In einer nächsten Sitzung des Programmes möchte ich die Dateien selbstverständlich auch wieder lesen (und die Funktionsvielfalt der Abgeleiteten Klasse wie vor dem Speichern haben):

C-/C++-Quelltext

1
2
3
4
5
6
7
std::ifstream Read(FilePath, std::ios::binary);
Block ActualBlock; //Objekt zum Speichern der eingelesenen Daten
Read.read((char*)&ActualBlock, sizeof(Block));
ActualBlock.SetMainData(GameInfo::BlockTextures); //Funktion um Pointer, die nicht gespeichert wurden, zu füllen (_vfptr bleibt fehlerhaft)
Block Copy = ActualBlock; //Bei jeglicher Zuweisung des Objektes kommt ein Runtime Error in xtree Zeile 1979,
                        //bei Verwendung der virtuellen Funktionen ein Runtime Error bei der jeweiligen Ausführung der Funktion.
Read.close();


Zunächst ein Leseversuch in der gleichen Sitzung:
Durch den Debugger habe ich erfahren, dass das Objekt vollkommen gut eingelesen wurde und es die gleichen Werte besitzt wie vorher, nur ist ein neues Element namens "[Block_Air]" hinzugekommen. Kann es vllt. sein, dass das eingelesene Objekt jetzt nur noch eine Block-Klasse ist, jedoch noch werte der Abgeleiteten Klasse hat? Innerhalb des Feldes sind alle Variablen, die es in der Klasse Block gibt nochmals vertreten, also praktisch zwei mal gespeichert. Das Objekt kann ich ganz normal benutzen, auch mit den Virtuellen Funktionen.

(Link)


Jetzt ein Leseversuch in einer späteren Sitzung:
Alle Eigenschaften der Klasse sind wieder wunderbar eingelesen, nur ist das Feld "[Block_Air]" nicht mehr vorhanden und in _vfptr wird kein Speicher mehr korrekt gelesen. Ich vermute es liegt daran, dass _vfptr auch in die Binärdatei geschrieben wurde (was ja nur Pointer zu den virtuellen Funktionen enthält (oder liege ich da falsch?)) und diese dann nicht mehr zu den Funktionen zeigen. Das würde auch erklären, warum es in der gleichen Sitzung noch Funktioniert. Wenn ich das Objekt kopieren möchte, wird ein Runtime Error erzeugt (s. Quellcode einlesen z. 5), jedoch kann ich Funktionen, die in der Basisklasse Block vorhanden sind, problemlos ausführen.


(Link)


Doch wie ändere ich, dass die virtuellen Funktionen wieder richtig ausgeführt werden? Ich kann ja _vfptr nicht im Quellcode bearbeiten, da es ja zur Kompilierzeiten erstellt wird. Oder liegt das Problem ganz woanders?
Ich benutze Visual Studio 2013 mit v120, Compileroptimierung deaktiviert.

mfg propper

David Scherfgen

Administrator

Beiträge: 10 382

Wohnort: Hildesheim

Beruf: Wissenschaftlicher Mitarbeiter

  • Private Nachricht senden

2

15.03.2015, 08:04

Du kannst ein Objekt, dessen Klasse virtuelle Methoden hat, nicht einfach so in eine Datei schreiben und erwarten, dass es nach dem Einlesen wieder funktioniert. Ja, das liegt an der Tabelle für virtuelle Methoden. Du musst dich selbst um die Serialisierung kümmern. Da gibt's z.B. bei Boost hilfreiche Libraries.

3

15.03.2015, 08:11

Serialisierung beim Speichern und Deserialisierung beim Laden.
Cube Universe
Entdecke fremde Welten auf deiner epischen Reise durchs Universum.

4

15.03.2015, 13:22

Mal davon abgesehen, solltest du auch den Destruktor von Block virtual machen, sonst kann es unter Umständen zu Memory Leaks kommen.

propper

Frischling

  • »propper« ist der Autor dieses Themas

Beiträge: 21

Wohnort: Internet

  • Private Nachricht senden

5

15.03.2015, 13:34

Boost serialization

Danke für den Tipp,
ich habe es jetzt mit Boost serialization versucht, doch immer wenn ich ein Objekt des Typs Block abspeichern möchte, sieht es so aus, als würde es Funktionieren (auch vom Dateiinhalt her), doch wenn ich es wieder einlese, kommt die "unregistered_class"-Ausnahme. Laut der Website fehlt ein Ekport_Key. Ich habe herausgefunden, dass der Export_Key ungefähr so aussehen kann (Quelle):

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
//So the header file export.hpp includes macro definitions to specify the external string used to identify the class. (GUID stands for Globally Unique IDentfier.)
BOOST_CLASS_EXPORT_GUID(my_class, "my_class_external_identifier")

//In a large majority of applications, the class name works just fine for the external identifier string so the following short cut is defined
BOOST_CLASS_EXPORT(my_class)

//which expands to:
BOOST_CLASS_EXPORT_GUID(my_class, "my_class")

//If the an external name is required somewhere in the program and none has been assigned, a static assertion will be invoked.

Doch was genau bringt das jetzt und wie füge ich es in mein Code ein? In Beispielen wie dem hier blicke ich dann gar nicht mehr durch 8|.

Boost habe ich wie folgt implementiert:

Basisklasse Block:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#pragma once
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/base_object.hpp>
[...]

class Chunk;

class Block
{
public:
    Block() {}; //Konstruktor
    ~Block() {}; //Destruktor

    void BaseHandleEvents();
    void BaseUpdate(float NewFrameTime);
    void BaseRender();

    virtual void HandleEvents() {};
    virtual void Update(float NewFrameTime) {};
    virtual void Render() {};

    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        //Beispiele:
        ar & AbsolutePosition;
        ar & ChunkPosition;
        ar & Size;
        [...]

        //noch zu laden:
        //Sprite und Texture (SetMainDatadurchlaufen)
    }

    inline void SetMainData(sf::Texture *NewBlockTexture) { ... }

    [...]
};


Ableitung Block_Air:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#pragma once
#include "Block.hpp"

class Block_Air : public Block
{
public:
    Block_Air();
    Block_Air(const Block &B) : Block(B) {};
    Block_Air(sf::Texture *NewBlockTextures);
    Block_Air(GameInfo::BlockProperties::BlockProperties BlockProperties);
    ~Block_Air() {};

    void HandleEvents();
    void Update(float NewFrameTime);
    void Render();

    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & boost::serialization::base_object<Block>(*this);
        //Muss hier evtl. noch irgendwas für die virtuellen Methoden hin? Und wird die Basisklasse auch richtig Serialisiert?
        //Da auf der Website "Note the serialization of the base classes from the derived class. Do NOT directly call the base class serialize functions." steht,
        //Bin ich mir jetzt nicht sicher, ob das Objekt als Basisklasse komplett gespeichert wird, oder ob doch die serialize-Methode aufgerufen wird.
    }
};


Speichern:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
std::ofstream ofs(FilePath);
Block SaveBlock = *ChunkMap.at(CoordinateX)->GetBlock(0, 0); //gibt Pointer vom Typ Block zu dem zu speichernden Block zurück (Kann eine von allen nach dem gleichen Prinzip von Block_Air abgeleiteten Klassen sein
boost::archive::text_oarchive oa(ofs);
// In Datei schreiben
try{
    oa << SaveBlock;
} catch(boost::archive::archive_exception& ex){
    std::cout << "Exeption: " << ex.code;
}


Lesen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
Block *LoadedBlock = new Block();
// Archiv erstellen
std::ifstream ifs(FilePath);
boost::archive::text_iarchive ia(ifs);
//Objekt einlesen
try{
    ia >> LoadedBlock;
    LoadedBlock->SetMainData(GameInfo::BlockTextures); //Funktion um Pointer, die nicht gespeichert wurden, zu füllen
} catch(boost::archive::archive_exception& ex){
    std::cout << "Exeption: " << ex.code;
}


@Malloc Ok, ich werde mal Googlen, habe ich noch nie gemacht :D

mfg propper

6

15.03.2015, 13:38

Boosz ist hier denk ich mal overkill. Lass deine klasse einfach ein Serialize(std::ostream&) und Deserialize(std::istream&) anbieten.

propper

Frischling

  • »propper« ist der Autor dieses Themas

Beiträge: 21

Wohnort: Internet

  • Private Nachricht senden

7

15.03.2015, 13:42

Meinst du damit das aufsplitten von serialize(...) in load(...) und save(...)?

8

15.03.2015, 15:13

Du rufts die 'serialize'-methode des objekts auf und die klasse schreibt ihre Member z.B. in einen std::ostream, den sie übergeben bekommen hat. Jetzt hast du das objekt irgendwo 'gespeichert'. Das gegenstück ist die 'deserialize'-methode, die aus den daten, die z.B. aus einem std::istream kommen wieder den zustand des objektes herstellt.

David Scherfgen

Administrator

Beiträge: 10 382

Wohnort: Hildesheim

Beruf: Wissenschaftlicher Mitarbeiter

  • Private Nachricht senden

9

15.03.2015, 16:15

Und warum ist das jetzt besser? Mit Boost brauchst du nur eine Methode zu implementieren, nicht zwei.

10

15.03.2015, 16:21

Ja, gut, da hast du recht, aber ich finde da ist Boost jetzt die Kanone, die auf Spatzen schießt ;)

Werbeanzeige