1. Változók élettartama

Hanoi tornyai

Adott az alábbi forráskód, amely a Hanoi tornyai feladványt oldja meg:

#include <iostream>
 
void hanoi(int n, char honnan, char seged, char hova) {
    if (n == 0)
        return;
    hanoi(n-1, honnan, hova, seged);
    std::cout << "rakj 1-et: " << honnan << "->" << hova << std::endl;
    hanoi(n-1, seged, honnan, hova);
}
 
int main() {
    hanoi(4, 'A', 'B', 'C');

    return 0;
}

Hogy lehet megoldani azt, hogy minden lépés mellé ki legyen írva annak sorszáma? Pl. 3 korongra így:

1. lepes: rakj 1-et: A->C
2. lepes: rakj 1-et: A->B
3. lepes: rakj 1-et: C->B
4. lepes: rakj 1-et: A->C
5. lepes: rakj 1-et: B->A
6. lepes: rakj 1-et: B->C
7. lepes: rakj 1-et: A->C

Mutass minél többféle, egymástól eltérő elvű megoldást! (A rekurzió maradjon meg.) Mi a megoldásokban a közös?

Figyelj arra, hogy az elkészített függvények használatának módja – lényegében a fejléce – ugyanaz legyen, mint az eredeti függvényé (korongok száma, oszlop1, oszlop2, oszlop3)! Ez persze nem jelenti azt, hogy nem írhatsz segédfüggvényeket ettől eltérő paraméterezéssel. Ne csinálj tárolót, nincs rá szükség: a függvény olyan sorrendben állítja elő a lépéseket, ahogy a kimenetre is azoknak kerülniük kell. A tároló kapcsán amúgy is meg kellene oldanod egy olyan problémát, ami anélkül is felmerül, és a feladat lényege.

2. Okos pointerek

A kifejezésfa okos pointerrel és egyszerűsítéssel

A kifejezések az előadáson bemutatott kódban egyszerűsíteni is tudták magukat. Pl. valami+0 = valami, konstans+konstans = konstans és hasonló minták felismerésével. Az összeadás osztálynál ez így nézett ki:

virtual Expression * simplify() const override {
    Expression * lhs_simpl = lhs_->simplify();
    Expression * rhs_simpl = rhs_->simplify();
    Constant * lhs_cons = dynamic_cast<Constant *>(lhs_simpl);
    Constant * rhs_cons = dynamic_cast<Constant *>(rhs_simpl);
    if (lhs_cons != nullptr && lhs_cons->get_value() == 0.0) {  /* 0 + a = a */
        delete lhs_simpl;   /* = 0 */
        return rhs_simpl;
    }
    if (rhs_cons != nullptr && rhs_cons->get_value() == 0.0) {  /* a + 0 = a */
        delete rhs_simpl;   /* = 0 */
        return lhs_simpl;
    }
    if (lhs_cons != nullptr && rhs_cons != nullptr) {   /* c + c = c */
        double new_value = lhs_cons->get_value() + rhs_cons->get_value();
        delete lhs_simpl;
        delete rhs_simpl;
        return new Constant{new_value};
    }
    return new Sum{lhs_simpl, rhs_simpl};
}

Láthatóan elég nagy gondot okozott a kézi memóriakezelés: ha valamelyik egyszerűsített részkifejezésre nem volt szükség (pl. mert 0 értékű), akkor explicite törölni kellett azt a delete operátorral.

Építsd be a labor feladatában elkészített programodba az egyszerűsítést! A konstans és a változó nem egyszerűsíthető (önmagát adja), az összeg és a szorzat viszont igen. Az std::shared_ptr-ekhez van egy dynamic_pointer_cast() segédfüggvény is a <memory> fejlécfájlban: dynamic_pointer_cast<X>(p) ugyanazt csinálja okos pointerekre, mint amit a dynamic_cast<X*>(p) nyers pointerekre.

A szorzat egyszerűsítése:

virtual Expression * simplify() const override {
    Expression * lhs_simpl = lhs_->simplify();
    Expression * rhs_simpl = rhs_->simplify();
    Constant * lhs_cons = dynamic_cast<Constant *>(lhs_simpl);
    Constant * rhs_cons = dynamic_cast<Constant *>(rhs_simpl);
    if (lhs_cons != nullptr && lhs_cons->get_value() == 0.0) {  /* 0 * a = 0 */
        delete rhs_simpl;
        return lhs_simpl;   /* = 0 */
    }
    if (rhs_cons != nullptr && rhs_cons->get_value() == 0.0) {  /* a * 0 = a */
        delete lhs_simpl;
        return rhs_simpl;   /* = 0 */
    }
    if (lhs_cons != nullptr && lhs_cons->get_value() == 1.0) {  /* 1 * a = a */
        delete lhs_simpl;   /* = 1 */
        return rhs_simpl;
    }
    if (rhs_cons != nullptr && rhs_cons->get_value() == 1.0) {  /* a * 1 = a */
        delete rhs_simpl;   /* = 1 */
        return lhs_simpl;
    }
    if (lhs_cons != nullptr && rhs_cons != nullptr) {   /* c * c = c */
        double new_value = lhs_cons->get_value() * rhs_cons->get_value();
        delete lhs_simpl;
        delete rhs_simpl;
        return new Constant{new_value};
    }
    return new Product{lhs_simpl, rhs_simpl};
}
Megoldás

A teljes megoldás: expression_shared_simplify.cpp.

SmartPtr fordítási hiba

Adott az alábbi intruzív okos pointer megvalósítás.

#include <iostream>


template <typename T>
class SmartPtr;


class RefCntd {
    int counter = 0;
    template <typename T> friend class SmartPtr;
};


template <typename T>
class SmartPtr {
    T* pobj;
    
  public:
    explicit SmartPtr(T* pobj = nullptr)
        : pobj(pobj)
    {
        if (pobj)
            ++pobj->RefCntd::counter;
    }
    SmartPtr(SmartPtr const &) = delete;    /* lusta */
    SmartPtr& operator=(SmartPtr const &) = delete;    /* lusta */
    
    ~SmartPtr() {
        if (pobj)
            if (--pobj->RefCntd::counter == 0)
                delete pobj;
    }
};

class X : public RefCntd {
  public:
    X() {
        std::cout << "Hello\n";
    }
    ~X() {
        std::cout << "Bye\n";
    }
};

int main() {
    SmartPtr<X const> ptr(new X);
}

A kód nem fordul. Miért? Egyáltalán kellene fordulnia, vagy koncepcionálisan is hibás? Ha érdemes javítani, akkor hogyan, és miért jogos az a javítás?

Megoldás

A probléma az, hogy a főprogramban az X const példányosítás miatt a SmartPtr adattagja X const * típusú lesz, és ezért a --pobj->RefCntd::counter kifejezés konstans változót próbálna módosítani.

A legjobb megoldás az, ha a referenciaszámlálót mutable adattaggá változtatjuk:

class RefCntd {
    int mutable counter = 0;
    /* ... */
};

A mutable jelző azt mondja, hogy az objektum const volta nem kell érvényes legyen az adattagra. Jelen esetben ez jogos, mert a referenciaszámláló ugyan technikailag része az objektumnak, de igazából cska metaadat.

3. Heterogén kollekció

Polimorf típus

Tudjuk, hogy heterogén kollekcióhoz szükségünk van egy indirekcióra az objektumok eltérő típusa és mérete miatt. Ez kényelmetlen, mert a plusz indirekció és az ismeretlen típus is nehézkessé teszi az objektumok érték szerinti kezelését. A másolás megoldható egy clone() segédfüggvénnyel, amit virtuális konstruktorként emlegetünk, még ha valójában nem is konstruktor. Az érték szerint kezeléshez pedig az előadáson készítettünk egy segédosztályt, amely másoláskor az objektum clone() függvényét hívja:

template <typename BASE>
class PolymorphicValue {
  private:
    BASE *ptr_;
  public:
    explicit PolymorphicValue(BASE *ptr = nullptr): ptr_{ptr} {}
    ~PolymorphicValue() {
        delete ptr_;
    }
    PolymorphicValue(PolymorphicValue const & masik) {
        if (masik.ptr_ != nullptr)
            ptr_ = masik.ptr->clone(); // itt a lényeg
        else
            ptr_ = nullptr;
    }
    /* ... */
};

Ezzel persze egy új elvárásunk van a tartalmazni kívánt típus felé: legyen clone() függvénye:

class Shape {
  public:
    virtual Shape * clone() const = 0;
    /* ... */
};
class Triangle: public Shape { /* ... */ };
class Rectangle: public Shape { /* ... */ };

PolymorphicValue<Shape> s1(new Rectangle);
PolymorphicValue<Shape> s2 = s1;        // p1.ptr_->clone()

Erre azért van szükség, mert a PolymorphicValue osztály konstruktora Shape * paramétert kap, és nem ismeri a lemásolandó objektum típusát. Az intruzív megvalósítás azonban nem előnyös: A PolymorphicValue feladata, a memóriakezelés egy része bekerült a Shape osztályba és annak leszármazottaiba.

Viszont ha más megközelítéssel indulnánk neki a feladat megoldásának, ismerhetné a típust. Ha sablon lenne a PolymorphicValue konstruktora, akkor átvehetné érték szerint is az objektumot, amelyet tartalmazni kell, és ismert lenne a típusa is:

PolymorphicValueV2<Shape> s1( Rectangle{} );    // template-es ctor
PolymorphicValueV2<Shape> s2( Triangle{} );

PolymorphicValueV2<Shape> s3 = s1;          // Rectangle copy ctor, Triangle copy ctor
PolymorphicValueV2<Shape> s4 = s1;

s3->get_area();                             // Shape* és Shape&
(*s3).get_area();

Oldd meg, hogy ez így működjön! Ehhez szükséged lesz egy type erasure-re is, amiről előadáson esett szó. Ha mindent jól csinálsz, a clone() függvény törölhető lesz; a PolymorphicValueV2 osztályod közvetlenül a tartalmazott objektum másoló konstruktorát fogja használni, és semmilyen egyéb függvényt nem vár annak interfészén.

4. Pontot érő szorgalmi feladatok az első nagy házihoz

Ezek a feladatok az első nagy házit egészítik ki. A jelzett feladatok plusz pontért adhatóak be.

Plusz pontért beadható: Helyettesítő objektum az indexeléshez

Tekintsük a következő kódrészletet:

MyString a = "hello";
MyString b = a;
std::cout << b[1];          /* e */

a[1] = 'a';
b[1] = b[4];
std::cout << a << " " << b; /* hallo hollo */

Ebben a másolat objektumot megindexeljük, ami hatására meghívódik az indexelő operátora. De a két indexelő közül (konstans és nem konstans) a nem konstans fog meghívódni, mivel a b objektumnak nincs const minősítője. Ez pedig sajnos különválasztja a két sztringet, pedig nem is módosul a b értéke az első kiírásnál. A probléma az, hogy az indexelés pillanatában még nem tudjuk, hogy a következő művelet egy írás vagy egy olvasás lesz, úgyhogy igazából az indexelő operátor még nem tudja eldönteni, hogy külön kell-e választani a két sztringet (copy-on-write) vagy nem.

A helyzetet úgy tudod megoldani, ha egy helyettesítő (proxy) objektumot használsz. Az indexelő operátornak nem egy karakter referenciájával kell visszatérnie, hanem egy olyan objektummal, amely a sztring megindexelt karakterét reprezentálja. Ez az objektum tudja, hogy melyik sztring melyik karakteréről szól. Az írás és az olvasás a helyettesítő objektumon már külön műveletnek fog látszani. Lehet olyan értékadó operátort írni neki, amely karaktert vesz át (erre ő a kapott karaktert beleírja a hivatkozott sztringbe, esetleg közben szétválasztja a sztringeket), és kaphat olyan konverziós operátort is, amely ezt karakterré alakítja – értelemszerűen ilyenkor kiolvasva a hivatkozott sztring hivatkozott karakterét.

Ha végeztél, ellenőrizd, hogy nem vétettél-e OOP tervezési hibát. A proxy objektumnak nem dolga dinamikus tömbökkel bíbelődni. Egyetlen feladata, hogy az írást és az olvasást megkülönböztesse; a tényleges memóriaműveleteket már a MyString osztálynak kell elvégeznie. Ha nem ilyen lett a kódod, refaktoráld!

Plusz pontért beadható: egyformán inicializált sztringek

Adott az alábbi kódrészlet:

MyString a = "helló világ";
MyString b = "helló világ";

Az eddigi kódváltozatban ez nem egy, hanem két tömböt hoz létre – tehát a memóriában kétszer van benne ugyanaz a tömb. Ez a redundancia elkerülhető, ha a létrehozáskor a sztringek mindig megnézik, hogy létezik-e már ugyanolyan tartalmú szöveg, mint amilyen értéket fel kell venniük. Ha igen, akkor elég csak azt meghivatkozni. Ehhez azonban ismerni kell az összes szöveg objektumot. Valamilyen módon, például egy láncolt listában (vagy jobb ötlet: keresőfában) nyilvántartást kell vezetni arról, hogy milyen szöveg objektumok léteznek. A feladat ezt implementálni; ehhez használhatsz STL tárolót is.

5. További szorgalmi feladatok az első nagy házihoz

Kivételkezelés

Vizsgáld meg exception safety szempontból, és ha kell, javítsd a MyString osztályodat! Valósítsd meg az összes sztringműveletet úgy, hogy erős garanciát, atomi működést biztosítsanak! Sehol nem szabad olyannak történnie, ha bármelyik memóriafoglalás (new vagy malloc) hibát jelez, akkor a sztring objektum megsérül, az abban tárolt adat elveszik, vagy memóriaszivárgás keleketkezik.

Az indirekciók számának csökkentése

Az alapértelmezett megvalósítás szerint a MyString objektumokhoz tartozó StringValue objektumokat dinamikusan foglaljuk. Így oldható meg, hogy azok száma független lehessen a MyString objektumok számától. A hivatkozott karaktertömböket pedig szintén dinamikusan kell foglalnunk, mert azoknak pedig a mérete lehet eltérő. Ezt mutatja a következő ábra, ahol a kékkel színezett dobozok a dinamikusan foglalt területeket jelzik.

Dinamikus memóriaterületek a MyString osztályban

Mindez nem túl hatékony. A foglaláshoz kétszer kell a new operátort használnunk. Egy adott karakter eléréséhez pedig két indirekción kell keresztülmennünk; az elsővel még csak a StringValue objektumhoz jutunk, amelyből egy újabb pointer kiolvasása és dereferálása árán kapjuk csak meg a karaktert.

A feladat ezen indirekciók számát csökkenteni. Ez a referenciaszámláló, az esetlegesen eltárolt sztringméret és a karakterlánc egy memóriaterületre helyezésével oldható meg. Mindez azt is jelenti, hogy az ehhez lefoglalt terület méretét a szöveg méretének figyelembe vételével kell kiszámítani:

Dinamikus memóriaterületek a MyString osztályban

A MyString objektumokban eltárolt pointerek így különböző méretű memóriaterületekre mutatnak, amelyek már érték szerint tartalmaznak minden adatot.