Heterogén kollekció perzisztenciája

Czirkos Zoltán · 2019.02.27.

Heterogén kollekció fájlba mentése és betöltése. A fájlba mentést könnyen megoldja egy virtuális save() függvény – de hogy lesz a load() is virtuális, ha azt sem tudjuk, milyen objektumot kell létrehozni?

C++-ban a perzisztenciát általában egy virtuális save() és load() függvénnyel valósítják meg, vagyis az egyes objektumok maguk gondoskodnak a fájlba mentésükről és visszatöltésükről.

Ha egy fájlban többféle objektum adatait tároljuk, akkor egy kicsit bonyolódik a helyzet. Minden objektum elé ki kell írni ugyanis egy nevet (pl. kör, téglalap), hogy a típusokat meg tudjuk különböztetni. A visszaolvasáskor pedig először be kell olvasni ezt a nevet, utána pedig egy esetszétválasztás következik: ha a név „kör”, akkor egy kör konstruktornak kell futnia; ha a név „téglalap”, akkor egy téglalap konstruktornak stb. Itt már gond van, ugyanis virtuális konstruktor nincs, vagyis a többalakúságot már nem tudjuk használni.

Az öröklésnél annyiban is bonyolult a helyzet, hogy szeretnénk egyrészt minél több működést megvalósítani az alaposztályban, másrészt pedig szeretnénk úgy megvalósítani az alaposztályt, hogy abból utána bármilyen további osztályokat le lehessen származtatni, de arról az alaposztálynak ne kelljen előre tudnia. A beolvasás az összes leszármazott osztálynál megvalósítandó tulajdonság, viszont az esetszétválasztást már nem rakhatjuk az alaposztályba, mert ott fel kell sorolni az összes leszármazott osztályt – akkor ugye azzal a kitétellel vagyunk gondban, hogy bármit kéne tudnunk származtatni az alaposztályból, annak „tudta nélkül”.

Ez a helyzet a következőképpen oldható meg.

Minden leszármazott objektum rendelkezik egy virtuális save() függvénnyel, amelyik valami ilyesmit ír a fájlba: „típus adat1 adat2 adat3”. Mindegyik rendelkezik a megfelelő virtuális load() függvénnyel, amelyik már csak az adatokat olvassa vissza egy meglévő objektumba: „adat1 adat2 adat3”. Nyilván egy meglévő példánynak kell lennie, mert csak így lehet virtuális a függvény – az objektumnak már léteznie kell, hogy a megfelelő virtuális függvénye meglegyen.

Hogy a virtuális jellegét ki tudjuk használni a load() függvénynek, az egyes objektumok létrehozásakor már léteznie kell az objektumnak, vagyis pontosabban, egy(!) olyan(!) objektumnak már léteznie kell. Ha létezik egy olyan objektum, akkor azt lemásolhatjuk (a típus persze itt is változhat, vagyis ennek is virtuálisnak kell lennie), és az adatokat a fájlból a másolatba beolvassuk. Az alaposztályban így a következő dolgot csináljuk:

  • A fájlból olvasott típus alapján előkerítjük az objektum prototípusát.
  • A prototípust megkérjük, hogy másolja le magát (virtuális).
  • A másolatba pedig beolvassuk az objektum adatait (virtuális).

Az alaposztálynak nem kell tudnia a leszármazott osztályokról! A prototípusokat egy tárolóba rakjuk, amelyik tároló a fájlba írt neveket képezi le a különféle objektumokra, nevezetesen az alaposztály pointerére. Ez praktikusan egy std::map<std::string, Alap*> lesz. A beolvasáskor az adott sztringre rákeresve az alaposztály így találja meg a prototípust; a konkrét típusról nem kell tudnia, mert mind a másolást, mind a beolvasást már a leszármazott osztályok virtuális függvényei csinálják. A prototípusok tárolóját pedig a program elején feltöltjük a megfelelő objektum példányokkal.

Látható, hogy így sem a heterogén kollekciókat (pl. alakzatokat tároló rajztábla objektum), sem az alaposztályt nem kell módosítani ahhoz, hogy egy új típust létrehozzunk. Az új típus létrehozása a szokásos módon történik: leszármaztatjuk az alaposztályból, megírjuk a virtuális függvényeit; az egyetlen többlet teendő a prototípus létrehozása a program elején.

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <map>
#include <stdexcept>


/* ALAP OSZTALY */
class Alakzat {
  protected:
    int x, y;
  public:
    virtual void kiir(std::ostream& os) const = 0;  /* magyarul */

    virtual void save(std::ostream& os) const = 0;  /* fajlba ment */
    virtual void load(std::istream& is) = 0;        /* visszatolt */
    virtual Alakzat* klonoz() const = 0;            /* masolat */

    virtual ~Alakzat() {}

    /* a prototipusok kezelese es az uj objektum eloallitasa */
};


class AlakzatKezelo {
  private:
    std::map<std::string, Alakzat*> prototipusok;

  public:
    ~AlakzatKezelo() {
        std::map<std::string, Alakzat*>::iterator it;
        for (it = prototipusok.begin(); it != prototipusok.end(); ++it)
            delete it->second;
    }

    void prototipus_hozzaad(Alakzat* a, char const *nev) {
        prototipusok[nev] = a;
    }

    Alakzat* beolvas(std::istream& is) {
        std::string tipus;
        if (!(is >> tipus))
            return NULL;    /* nincs tobb adat */
        std::map<std::string, Alakzat*>::iterator it;
        it = prototipusok.find(tipus);
        if (it == prototipusok.end())
            throw std::runtime_error("ervenytelen alakzat tipus!");

        Alakzat* proto = it->second;    /* a prototipus a map-bol */
        Alakzat* uj = proto->klonoz();  /* lemasoljuk a prototipust */
        uj->load(is);                   /* a masolat beolvassa magat */
        return uj;                      /* es azt adjuk vissza */
    }
};


/* LESZARMAZOTTAK */
class Teglalap: public Alakzat {
  private:
    int szel, mag;
  public:
    Teglalap() {}   /* a prototipusok miatt szuksegunk van default konstruktorra */
    Teglalap(int x1, int y1, int x2, int y2) {
        x = x1; y = y1;
        szel = x2-x1; mag = y2-y1;
    };
    void kiir(std::ostream& os) const {
        os << "Teglalap vagyok, itt: " << x << ',' << y << ", "
           << szel << 'x' << mag << std::endl;
    }
    void save(std::ostream& os) const {
        os << "rectangle" << ' ' << x << ' ' << y << ' '
           << szel << ' ' << mag << std::endl;
    }
    void load(std::istream& is) {
        is >> x >> y >> szel >> mag;
    }
    Teglalap* klonoz() const {
        return new Teglalap(*this);
    }
};


class Kor: public Alakzat {
  private:
    int sugar;
  public:
    Kor() {}
    Kor(int xk, int yk, int r) {
        x = xk; y = yk;
        sugar = r;
    };
    void kiir(std::ostream& os) const {
        os << "Kor vagyok, itt: " << x << ',' << y
           << ", r=" << sugar << std::endl;
    }
    void save(std::ostream& os) const {
        os << "circle" << ' ' << x << ' ' << y
           << ' ' << sugar << std::endl;
    }
    void load(std::istream& is) {
        is >> x >> y >> sugar;
    }
    Kor* klonoz() const {
        return new Kor(*this);
    }
};


int main() {
    /* a program elejen eloallitjuk a prototipusokat. */
    AlakzatKezelo ak;
    ak.prototipus_hozzaad(new Teglalap, "rectangle");
    ak.prototipus_hozzaad(new Kor, "circle");


    /* es akkor a kiprobalas innentol kezdve: ------------- */
    std::vector<Alakzat*> v;
    v.push_back(new Teglalap(20, 30, 50, 70));
    v.push_back(new Kor(30, 50, 10));

    std::cout<<"Ezek az alakzatok vannak: -----"<<std::endl;
    for (size_t i = 0; i < v.size(); ++i)
        v[i]->kiir(std::cout);
    std::cout<<std::endl;



    /* elmentem oket egy "fajlba". */
    std::ostringstream os;
    for (size_t i = 0; i < v.size(); ++i)
        v[i]->save(os);

    std::cout << "A fajl tartalma: -----" << std::endl;
    std::cout << os.str();
    std::cout << std::endl;



    /* elfelejtem oket */
    for (size_t i = 0; i < v.size(); ++i)
        delete v[i];
    v.clear();
    std::cout<<"A vektor most ures."<<std::endl<<std::endl;



    /* beolvasom oket a "fajlbol" */
    std::istringstream is(os.str());
    Alakzat* uj;
    while ((uj = ak.beolvas(is)) != NULL)
        v.push_back(uj);

    /* ujra kiir mindent */
    std::cout << "Ezeket olvastam a fajlbol: -----" << std::endl;
    for (size_t i = 0; i < v.size(); ++i)
        v[i]->kiir(std::cout);
    std::cout << std::endl;
    for (size_t i = 0; i < v.size(); ++i)
        delete v[i];
}