3. hét: Objektumok memóriaképe

Czirkos Zoltán · 2022.06.21.

1. Az előadással kapcsolatosan...

std::hex, std::setw, std::fill

Az előadás objektum bájtjait kiíró programjában...

Hogy lehet printf()-fel megoldani a ciklusban lévő körülményes kiírást?

Megoldás
printf("%02x ", ptr[i]);

A referencia mérete

Az előadás objektum bájtjait kiíró programjában...

Nem rossz ez a program? A sizeof(obj) nem a referencia méretét adja? Hiszen az obj paraméter típusa konstans referencia. C-ben nem volt szabad ilyet csinálni: ha indirekt módon, pointerrel adtunk át egy paramétert (pl. tömböt), akkor a sizeof mindig a pointer méretét adta. Itt mi a helyzet?

Megoldás

Nincs olyan, hogy „referencia mérete”. Egy objektum referenciája, azaz neve, mindenben megegyezik az eredeti objektummal, tökéletesen megkülönböztethetetlen attól. Ezért sizeof(obj)==sizeof(T) a függvényben. A referencia nem objektum, és így nincs definiált mérete sem.

Érték kiírása

Az előadás objektum bájtjait kiíró programjában...

Működik az, ha nincs változó, csak egy értéket szeretnék kiírni a fenti függvénnyel? Pl. helyes a print_obj_hex(1234); sor? Miért?

Megoldás

Igen, működik. A konstans referencia típusú paraméterek át tudnak venni jobbértékeket is, tehát olyan értékeket, amelyek nem változóban vannak tárolva, nincs nevük. Ilyenkor a fordító a hívás idejére létrehoz egy ideiglenes változót, és beteszi az értéket, hogy a függvény referenciaként megkaphassa azt.

Struct és class

Mi a különbség a class és a struct között?

Megoldás

Csak annyi, hogy a class-nál az adattagok (és az öröklések) alapértelmezetten privátak, a struct-nál publikusak. Ezért a struct Rectangle : Shape egy publikus leszármazást jelent.

Öröklés és tartalmazás

Ha az öröklésnél / tartalmazásnál is az objektum elejére kerül az ős objektum / tartalmazott objektum, akkor tulajdonképp mi a különség az öröklés és a tartalmazás között?

Megoldás

A memóriaképben ugyan semmi, de ez nem jelenti azt, hogy az öröklés és a tartalmazás között jelentésbeli különbség nincsen. Az öröklés „X egyfata Y”, a tartalmazás „X-nek van egy Y-ja” kapcsolatot fejez ki, és ez a programkód jelentése szempontjából sokat számít. Ettől függ az, hogy milyen konverziókat, paraméterátadásokat enged meg a fordító. Például nem származtathatunk egy pont típusból (x, y koordináták) téglalap típust azzal a céllal, hogy a téglalapnak legyen x és y koordinátája, mert akkor minden pontot váró függvénynek téglalapot is adhatunk paraméterként. A pont és a téglalap között nem leszármazási, hanem tartalmazási kapcsolat van: a téglalap nem egy pont, hanem a téglalapnak van egy pontja, amely a helyzetét meghatározza.

Offsetof I.

Az előadás programjában, a case TYPE_STRING részben mit jelent a char*, és mit a char** cast? Melyik miért ilyen?

Megoldás

Az „objektum pointere plusz ofszet” kifejezés által egy pointer kapunk az adott adattagra. Mivel a pont neve esetén az adattag típusa char*, az így kapott pointer char** típusú. A (char*) cast pedig a bájtonkénti címzés miatt kell.

Offsetof II.

Az előbb említett kódban, az adattagokat elérő, *(valami + valami.ofszet) alakú kifejezések elég sok zárójelet tartalmaznak. Elhagyható-e valamelyik zárójelpár, és miért?

Megoldás

((char*) &p) – itt elhagyható a külső pár. A következő operátor az összeadás, amely alacsonyabb precedenciájú, mint a (char*) cast.

A sok zárójel

Tényleg kell a zárójel az előadás kódjában a point_class_members[i].mptr köré?

Megoldás

Nem, csak nagyon nehéz olvasni nélküle. A .* operátor alacsonyabb precedenciájú, mint a [] és . operátorok, ezért a zárójel elhagyható lenne. Egy ilyen kifejezésben viszont látszik, hogy a .* jobb oldali operandusa egy teljes kifejezés lehet, szemben a sima . operátorral, amelynek a jobb oldali „operandusa” egy adattag neve lehet, semmi más.

Keresztkérdések

Az alábbi elméleti kérdésekre szövegben válaszolj, esetleg pár soros kódrészlettel illusztrálva, ha úgy látod hasznosnak! (Megjegyzések: 1. Az is lehet, hogy értelmetlen valamelyik kérdés. 2. Saját véleményt, meglátást írj, ne guglizz :D)

  1. Ha egyetlen egy mondatban kell megfogalmaznod, hogy mi a közük C-ben a pointereknek a tömbökhöz, mi lesz az az egy mondat?
  2. Hány bájtos egy referencia?
  3. Létezik-e referenciák tömbje? Vajon miért?

std::optional

Néha szükségünk van egy olyan objektumra, amelyik egy jól meghatározott típusú értéket tárol – vagy éppenséggel üres. Például egy függvénynél, amelyik visszaad egy értéket, vagy nem ad vissza semmit. Ha sikerül beolvasnia egy számot, akkor visszaadja azt, ha nem, akkor pedig csak egy üres objektumot.

Ezt meg tudjuk oldani dinamikus memóriakezeléssel is. De ebben az esetben az kényelmetlen és lassú: egyetlen egy int-et kellene dinamikusan foglalni, indirekten visszaadni stb. Ehelyett inkább egy olyan objektumra lenne szükségünk, amelyik ezt érték szerint tartalmazza, és tudja azt is, hogy üres-e vagy nem. A használatát így képzelhetjük el:

Optional<std::string> get_string_from_stdin();

int main() {
    Optional<std::string> opt = get_string_from_stdin();
    if (opt) {
        std::cout << "Ezt a sztringet kaptam: " << *opt << std::endl;
    } else {
        std::cout << "Nem kaptam sztringet" << std::endl;
    }
}

A tárolt érték típusa std::string, de a sablonparaméter miatt lehetne bármi más.

Készíts egy ilyen osztályt! Ügyelj az alábbiakra:

  • Nem használhatsz dinamikus memóriakezelést, mert akkor elveszik a feladat értelme.
  • Helyette egy sizeof(T) méretű tömböt kell az Optional objektumba tenni, és neked kell tudnod, hogy ott épp létre van hozva objektum vagy nincs.
  • Ügyelni kell arra is, hogy a tömb megfelelő módon helyezkedjen el a memóriában. Előfordulhat, hogy a T típusra vonatkoznak ilyen megkötések (pl. int típus 4-gyel osztható memóriacímen). A memóriabeli igazítás az alignas kulcsszóval oldható meg.
  • Az Optional objektum megszűntekor a tartalmazott T destruktorát is meg kell hívni, már amennyiben volt tartalmazott T.

Tesztesetek nincsenek megadva, azokat is neked kell kitalálnod. Vajon mi várható el egy ilyen osztálytól? Gondolj arra, hogy egy Optional 0 vagy 1 darab objektumot tartalmaz érték szerint, és azt az objektumot helyettesíti. Pl. másoláskor lemásolja azt. Érdemes a kódodat GCC/Clang -fno-elide-constructors fordítási paraméter mellett tesztelni, hogy kikapcsold a hibákat elfedő optimalizációkat!

A C++17 óta egyébként van ilyen osztály, std::optional néven. Ez a Boost optional osztály mintájára készült.

Megoldás
#include <iostream>
 
template <typename T>
class Optional {
  private:
    alignas(T) unsigned char storage[sizeof(T)];
    /* ha van benne objektum, ez == storage. ha nincs, akkor nullptr.
     * lehetne egy bool is, de így egy csomó cast elkerülhető. */
    T* obj;
    
  public:
    Optional() {
        obj = nullptr;
    }
        
    Optional(T const & newdata) {
        obj = new (storage) T(newdata);
    }
    
    ~Optional() {
        if (obj)
            obj->~T();
    }
 
    Optional(Optional const & other) {
        if (other.obj)
            obj = new (storage) T(*other.obj);
        else
            obj = nullptr;
    }
    
    Optional& operator=(Optional const & other) {
        if (this == &other)
            return *this;
 
        if (obj && !other.obj) {
            obj->~T();
            obj = nullptr;
        } else if (!obj && other.obj) {
            obj = new (storage) T(*other.obj);
        } else if (obj && other.obj) {
            *obj = *other.obj;
        } else {
            /* mindkettő uninitialized, nincs teendő */
        }
        return *this;
    }
 
    explicit operator bool () const {
        return obj != nullptr;
    }
   
    T & operator*() {
        if (!obj)
            throw std::runtime_error("empty optional");
        return *obj;
    }

    T const & operator*() const {
        if (!obj)
            throw std::runtime_error("empty optional");
        return *obj;
    }
 };
 
 
Optional<std::string> get_word_from_stdin() {
    std::string s;
    if (std::cin >> s)
        return s;
    else
        return {};
}
 
int main() {
    Optional<std::string> opt = get_word_from_stdin();
    if (opt) {
        std::cout << "Ezt a szót kaptam: " << *opt << std::endl;
    } else {
        std::cout << "Nem kaptam szót" << std::endl;
    }
}

2. Reflektivitás

Reflektivitás – a tevékenység virtuális függvényben

A kiindulási alap az extrákban megjelenő írás kódja.

A programot át lehetne írni úgy, hogy a kiírásokat végző dynamic_cast sorozat helyett a PointMptrBase osztályba egy virtuális függvényt teszünk:

class PointMptrBase {
  public:
    virtual ~PointMptrBase() {}
    virtual void save_to_stream(std::ostream & os, Point const& p) const = 0;
};


template <typename T>
class PointMptr : public PointMptrBase {
  public:
    PointMptr(T Point::* mptr) : mptr(mptr) {}
    T Point::* mptr;
    virtual void save_to_stream(std::ostream & os, Point const& p) const {
        os << p.*mptr;
    }
};

int main() {
    /* ... */

    for (int i = 0; point_class_members[i].name; ++i) {
        std::cout << point_class_members[i].name << " = ";

        point_class_members[i].mptrobj->save_to_stream(std::cout, p1);

        std::cout << std::endl;
    }

    /* ... */
}

Mi a véleményed erről az átalakításról? Vannak előnyei, vannak hátrányai?

Megoldás
  • Megszűnt a dynamic_cast, automatikusan megíródnak a függvények a sablonból.
  • Ha más működés kell bizonyos típusú adattagnál, egy adott template <typename T> class PointMptr osztály specializálható.
  • A PointMptrBase osztályba bekerült egy olyan tevékenység, amihez annak az osztálynak nincs köze (az adattag kiírása). Ide kerülne az összes többi tevékenység is; pl. dialógusablak létrehozásakor a szövegbeviteli mező, csúszka stb. elemek létrehozása. Ezek nem igazán tartoznak ehhez az osztályhoz. Ez feladatspecifikus, nem tartozik egy reflektivitást biztosító keretrendszerhez.

Reflektivitás – dynamic_cast

A kiindulási alap az extrákban megjelenő írás kódja.

Mi a helyzet ezzel a dynamic_cast() sorozattal, meg lehetne kerülni?

Megoldás

Meg, mint mindig. Betehetjük az MptrType<T> típusokba a teendőket. Esetleg használhatjuk a látogató mintát is, hogy külön osztályba kerülhessenek.

Reflektivitás – Visitor

Ez is az az extrákban megjelenő írás kódjára vonatkozik.

Oldd meg látogató mintával (Visitor pattern) a reflektív objektumok bejárását, az egyes adattagokon elvégzendő tevékenységeket!

Megoldás

Nincs mintamegoldás, de ha csináltál egyet, küldd el :D