4. hét: C++11 osztályhierarchiák

Czirkos Zoltán · 2022.06.21.

1. Kifejezésfák: az előadáshoz

Strucc

Igaz-e az, hogy a strucc egyfajta madár?

Megoldás

Definíció kérdése. Biológiából igen, de OOP-ben általában nem. Ugyanis ha a Bird osztálynak van egy fly() tagfüggvénye, akkor az Ostrich osztálynak is lesz fly() tagfüggvénye. Ha minden madár tud repülni, és a strucc egy madár, akkor a strucc is tud repülni. Ezért OOP-ben az Ostrich osztály nem szabad a madárból származtassa magát, különben megsérti azt a szerződést, amelyet a Bird osztály a használóival kötött (ti. hogy meg lehet hívni az objektumon a fly() tagfüggvényt, és az értelmes dolgot fog csinálni).

Kör

Igaz-e az, hogy a kör öröklődik az ellipszis osztályból?

Megoldás

Mint a struccos példa: matekból igen, OOP-ből nem. Ha az ellipszis ígér olyat, amit a kör nem tud betartani (például hogy van set_size(width, height) tagfüggvénye), akkor a kör nem lehet ellipszis.

Ellipszis

Származtatható-e az ellipszis a kör osztályból?

Megoldás

Örökölné a sugarát lekérdező függvényt is. Ha pl. ilyen van, akkor semmiképp.

Ellipse e1{2, 3};
std::cout << e1.get_radius();   // ?!

Most vexing parse

Tulajdonképp mi lesz itt a c2, hogyan érti ezt a sort a fordító?

Complex c2(Complex(c1), Complex());
Megoldás

Amikor a C++ egy sornál választhat, hogy egy objektum definíciójáról vagy egy függvény deklarációjáról van szó, mindig az utóbbit részesíti előnyben. A c2 ezért egy függvény lett, amely két paraméterű, és Complex visszatérési értékű. Az első paraméter típusa Complex, neve pedig c1. (Igen, és a zárójelet, mivel nem módosítja a jelentést, feleslegesnek ítéli a fordító. Próbáld ki: int (main)(int (argc), char **(argv)){}!) A második, névtelen paraméternél egy függvény típust adunk meg; a függvénynek nincs paramétere, a visszatérési típusa Complex. A tömbökhöz hasonlóan a paraméterként átadott függvényeket pointerként kezeli a fordító. Ezek alapján a c2-es sor ezzel egyenértékű:

Complex c2(Complex param1, Complex (*param2)());

Így már jobban látszik, hogy ez egy függvény.

inline virtuális függvény?

Lehet egy virtuális függvényt osztályon belül, inline definiálni? Úgy tűnhet, semmi értelme, mert a virtuális függvényt nem lehet a hívás helyén kifejteni: a hívása mindig indirekt, a virtuális függvénytábla pointerén keresztül történik.

Megoldás

A fenti példákban a szabatosság kedvéért vannak az osztályon belül megírva a függvények. Ezt meg szabad tenni a virtuális függvényeknél is. Mivel a fordító tudja, hogy virtuális függvényről van szó, tudja azt is, hogy a virtuális függvénytábla pointere miatt muszáj nem inline változatot is készítenie belőle. A hívás általában a függvénytáblán keresztül történik, azonban lehetséges olyan helyzet, ahol már fordítási időben ki lehet választani a függvényt, és így akár kódbeillesztéssel is megvalósítható a hívás:

Constant c1{5.1};
c1.print(std::cout);

Itt például pontosan ismert a c1 objektum típusa.

Inicializálás

Mi a különbség az alábbi két inicializálás között?

Complex c1{2.3, 3.4}, c2 = {2.3, 3.4};
Megoldás

Semmi.

Üres objektum

Kérdések az előadás Variable osztályával kapcsolatban.

Egy Variable objektumban nincs adattag, üres. Lehet az, hogy semennyi helyet nem foglal a memóriából?

Megoldás

Tartalmaz egy pointert, amelyik a virtuális függvénytáblájára mutat. Úgyhogy nem üres, csak nincsenek a felhasználó által is látható adattagjai.

Van a Variable osztálynak saját destruktora?

Megoldás

Van, a fordító írt neki. Egy olyat, ami meghívja azt adattagok destruktorait (bár most nincsenek adattagok), utána pedig meghívja az ős destruktorát.

Virtuális függvények száma

Az előadás TwoOperand osztályával kapcsolatos kérdés.

Egyre több a virtuális függvény; a kétoperandusú osztályban most lett két új. Nem lesznek ettől túl nagyok a kétoperandusú objektumok?

Megoldás

Nem. Az objektumokban nincsenek virtuális függvényekre mutató pointerek, hanem csak egy virtuális függvénytáblára mutató pointer van (amely lényegében egy pointereket tartalmazó struktúra). Így ha már volt egy virtuális függvény, további virtuális függvények hozzáadásának nincs semmilyen plusz költsége.

Nem virtuális interfész

Az előadás kifejezésfáival kapcsolatos kérdés.

Ha az osztály publikus interfészén nem kellene virtuális függvényeknek lenniük, akkor az Expression osztálynak miért vannak ilyenjei? Tudsz példákat mondani arra, hogy a kiértékelésnél és a kiírásnál miért kellene, hogyan lehetne a függvénysablon tervezési mintát alkalmazni?

Megoldás

A print() metódus és a globális operator<< (amely szinte része az osztálynak) már most is ilyen. A print() lehetne privát, a kiíró operátor pedig friend, és pl. minden kiírt kifejezés elé odaírná, hogy f(x)=. A leszármazottak pedig ebből tudnák, hogy az nem az ő dolguk.

Az evaluate() is lehetne privát, mondjuk do_evaluate() néven. A publikus, nem virtuális evaluate() pedig ellenőrizné, hogy nem hibás-e a paraméterként kapott double x, mielőtt ténylegesen belekezd a kiértékelésbe. Például hogy nem inf vagy nan értékű-e.

Különbség és hányados

Az előadás kifejezésfáival kapcsolatos feladat.

Implementálj különbség és hányados osztályokat!

Megoldás

Mint az összeg és a szorzat, csak - és / operátorokkal.

Függvény osztály

Az előadás kifejezésfáival kapcsolatos feladat.

Tervezz és implementálj egy általános függvény osztályt! Hova illik ez be az osztályhierarchiába? Implementálj egy szinusz és egy koszinusz osztályt! Ha ügyesen tervezted meg a függvény osztályt, akkor ezek az összeghez és a szorzathoz hasonlóan pár sorból fognak állni.

Megoldás

A függvény osztály hasonló a kétoperandusúhoz. Pl. sin(2x): a kiértékeléskor ki kell értékelni a belül tárolt kifejezést (2x), utána pedig alkalmazni kell a függvényt (sin). A kiírásnál pedig ki kell írni a függvény nevét, zárójel, belső kifejezés, zárójel.

Ha kész vannak a függvény, szinusz és koszinusz osztályok, akkor most írd meg azok deriválását is! Figyelj arra, hogy az összetett függvény deriválási szabályát kell alkalmaznod: f(g(x))'=f'(g(x))·g'(x), mert a szinusz vagy a koszinusz objektum is egy kifejezést tartalmaz. Használd a függvénysablon tervezési mintát! Próbáld meg a függvény osztályban megvalósítani, amit csak lehet, hogy a leszármazottak minél egyszerűbbek legyenek.

Válaszd külön az interfészeket és az implementációkat! Az egy fájlból álló programot bontsd szét fejlécfájlokra és forrásfájlokra! Tartsd be a C++ projekteknél megszokott elvet: egy osztály, egy forrásfájl.

Virtuális konstruktor

Miért nem lehet egy konstruktor virtuális?

Megoldás

Amíg még létre sem jött az objektum, nincs virtuális függvénytábla. A virtuális konstruktor tervezési mintát így csak meglévő objektumokkal tudjuk használni.

Egyszerűsítés

Az előadás kifejezésfáival kapcsolatos feladat.

Írd meg a többi osztály, pl. különbség, függvény, egyszerűsítő függvényeit is! Vigyázz, a különbség és a hányados nem kommutatív! Vajon a függvény osztályban az egyszerűsítést mennyire lehet általánosan megfogalmazni?

2. Kifejezésfák: a laborhoz

Függvény rajzolása

Az SVG (scalable vector graphics) egy ún. vektorgrafikus képformátum. Ebben nem képpontok vannak megadva, hanem egyszerű síkidomok (szakasz, kör stb.), és a képnéző program dolga megrajzolni a képet. Egy SVG fájl, amely néhány szakaszt tartalmaz, így néz ki:

<svg width="320" height="80" xmlns="http://www.w3.org/2000/svg">
  <line x1="30" y1="17.9136" x2="0" y2="42.9137" stroke="blue" />
  <line x1="60" y1="21.0799" x2="30" y2="17.9136" stroke="blue" />
  <line x1="90" y1="48.3747" x2="60" y2="21.0799" stroke="blue" />
  <line x1="120" y1="64.9893" x2="90" y2="48.374" stroke="blue" />
</svg>

Írj egy olyan programrészt, amely kirajzolja a beolvasáskor kapott függvényt! Ehhez nincs más dolgod, mint az x tengelyen haladva kiértékelni néhány helyen (mondjuk –5-től +5-ig, 0,1 lépésközzel), és szakaszokkal összekötni a kapott pontokat. Figyelj arra, hogy valamennyire ki kell nagyítanod a képet (hogy ne egy képpontnyi legyen az egység, mert akkor szinte semmi nem fog látszani), és a matematikai origót a kép közepére tenned (a fenti 320×80-as méretű képnél pl. x+160 és y+40). Az SVG-ben az y koordináta fentről lefelé nő, ezért a kapott y koordinátákat negáld a nagyítás és az eltolás előtt!

Tetszőlegesen használhatsz C (fprintf) vagy C++ (ostream) fájlkezelést is. A C-s talán egyszerűbb, mert a koordináták helyére %f-et írhatsz, és egy nagy, nem pedig sok apró sztringet kell megadnod. A C++11-ben használhatsz ún. raw string literal-okat, amelyeknél nem kell külön kezelned az idézőjelet és a visszapert. A legegyszerűbb raw string literal R"( karakterekkel kezdődik és )" karakterekkel fejeződik be:

std::cout << R"(Hello idézőjelben "\n" sortörés!)";
Hello idézőjelben "\n" sortörés!

Így könnyedén megadhatod az idézőjelekkel teli sztringeket:

fprintf(fp, R"(<svg width="320" height="80" xmlns="http://www.w3.org/2000/svg">)");

A kapott fájlt egy böngészőprogrammal, vagy szinte bármelyik képnéző programmal meg tudod nyitni.

0.5 x x x * * *   0.8 x x * *   +   0.5*x*x*x + 0.8*x*x
Megoldás
#include <cstdio>


void draw(Expression *e, char const *filename, char const *color) {
    FILE *fp = fopen(filename, "wt");
    double w = 320, h = 240;
    double zoom = 20;
    fprintf(fp, R"(<svg width="%f" height="%f" xmlns="http://www.w3.org/2000/svg">)", w, h);
    double dx = 0.1;
    for (double x = -5.0; x <= +5.0; x += dx) {
        double x1 = x;
        double y1 = e->evaluate(x1);
        double x2 = x + dx;
        double y2 = e->evaluate(x2);
        
        x1 = x1 * zoom + w/2;
        y1 = y1 * -zoom + h/2;
        x2 = x2 * zoom + w/2;
        y2 = y2 * -zoom + h/2;
        fprintf(fp, R"(<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="%s" />)", x1, y1, x2, y2, color);
    }
    fprintf(fp, R"(</svg>)");
}

Kifejezésfa rajzolása

A graphviz nevű programmal könnyedén lehet gráfokat rajzolni. Csak meg kell neki adni a csúcsokat és az éleket, és automatikusan elhelyezi őket egy képen – figyelve arra is, hogy szimmetrikus gráfokat rajzoljon, ha lehet. Egy egyszerű irányított gráfot így lehet neki megadni:

digraph ketto_plusz_harom {
    csucs_egy [label="+"];      // csúcs: név és attribútumok
    csucs_ketto [label="2"];
    csucs_harom [label="3"];
    csucs_egy -> csucs_ketto;   // él: honnan -> hova
    csucs_egy -> csucs_harom;
};

Az így megadott gráfból az alábbi módon, parancssorból könnyen lehet SVG fájlt csinálni. (Itt egy online változat is: http://sandbox.kidstrythisathome.com/erdos/.)

$ dot proba.dot -Tsvg -o proba.svg

Írd át úgy a kifejezéseket beolvasó programot, hogy a felépített kifejezésfát ki tudja írni egy ilyen dot fájlba! Két dologra kell figyelned:

  • A kifejezésfát rekurzívan be kell járnod, hogy lásd az összes csúcsot. Egy alkalmasan megadott virtuális függvény írhatja ki ilyenkor a csúcsok adatait egy fájlba, a csúcs típusától függő felirattal. A kétoperandusú objektum (összeg, szorzat) látja a gyerekeit, ezért ő könnyen ki tudja írni az éleket megadó sorokat.
  • A csúcsoknak egyedi azonosítót kell adnod. Ehhez egy egyszerű trükköt használhatsz itt, amelynek alapja az, hogy C++-ban minden objektum memóriacíme egyedi. Ha a csúcs nevébe beteszed a csúcsot jelképező objektum memóriacímét, akkor bármikor bármelyik csúcs nevét könnyen előállíthatod:
    Obj x;
    std::cout << "csucs_" << &x;
    csucs_0x7fffd531a684
    

    Ez pont jó lesz névnek.

Ne felejtsd megadni az új virtuális függvény override és final minősítőit, ahol kell!

Megoldás

Egy lehetséges megoldás a következő.

class Expression {
  public:
    /* ez írja ki egy csomópont adatait; a this értéke bekerül a csomópont nevébe.
     * a leszármazottak get_label() tagfüggvénye a feliratot fogja megadni. */
    virtual void dot(std::ostream &os) const {
        os << "node_" << this << " [label=\"" << get_label() << "\"];\n";
    }
  private:    
    virtual std::string get_label() const = 0;
};

/* így néznek ki a get_label() függvények */
class Constant : public Expression {
    std::string get_label() const {
        std::ostringstream os;
        os << c_;
        return os.str();
    }
};

class Variable: public Expression {
    std::string get_label() const {
        return "x";
    }
};

/* a kétoperandusú műveletek a csomópont adatai mellett kiírják
 * az éleket is, és meghívják a gyerekek kiíró függvényét is. */
class TwoOperand : public Expression {
    /* write to dot file */
    virtual void dot(std::ostream &os) const final {
        Expression::dot(os);
        os << "node_" << this << " -> " << "node_" << lhs_ << ";\n";
        os << "node_" << this << " -> " << "node_" << rhs_ << ";\n";
        lhs_->dot(os);
        rhs_->dot(os);
    }

    /* az eddigi kiírás miatt már úgyis van ez a függvényünk,
     * ami megadja az operátort. az jó lesz feliratnak. */
    std::string get_label() const final {
        char str[] = { get_operator(), '\0' };
        return str;
    }
};