5. hét: Típusok használata

Czirkos Zoltán · 2022.06.21.

1. Típusok használata

Indexelő objektum

Írj egy osztályt, amely egy 32 bites, előjel nélküli egészben: uint32_t (#include <cstdint>) tud tárolni 32 igaz/hamis értéket! Az objektumot lehessen indexelni, és indexelésen keresztül valamelyik bit értékét lekérdezni és beállítani!

BoolArray b;        /* 32 db false */
std::cout << b[3];  /* 0 = false */
b[3] = true;
std::cout << b[3];  /* 1 = true */

A nehézséget itt az jelenti, hogy a biteket tároló egész szám egyes bitjeire nem létezik referencia, és emiatt az indexelő operátor nem térhet vissza egyszerűen egy bool& típussal. Helyette egy segédobjektummal kell visszatérnie, amely saját típussal rendelkezik, és amely a lekérdezést és az értékadást megfelelően átdefiniált operátorokkal kezelni tudja.

Ha már működik a fenti, oldd meg, hogy a bitek közötti értékadás is működjön!

b[3] = true;
b[2] = b[3];
std::cout << b[2];  /* 1 = true, mert b[3] = true */

Indexelő objektum – hibás gondolatmenet, de működik?

Adott az előző feladatnak az alábbi megoldása. A kód csak a segédobjektumot mutatja vázlatosan, jelenleg csak ez a lényeges rész.

class BitArray::Proxy {
  private:
    BitArray &ba;
    int idx;

  public:
    Proxy(BitArray &ba, int idx): ba(ba), idx(idx) {}

    operator bool () const {
        return ba.get(idx);
    }

    Proxy& operator=(bool b) {
        ba.set(idx, b);
        return *this;
    }

    Proxy& operator=(Proxy &other) = delete;
};

Ez egy hibás elgondolást mutat be. Mi az? És hogyan lehetséges, hogy a Proxy mégis működőképes így megvalósítva?

Megoldás

A hibás elgondolás az, hogy ennek törölve van az értékadó operátora. Ugyanis a két Proxy közötti értékadás lehetséges: bitarray[0] = bitarray[1], ennek működnie kell. Az első bitet kell másolnia a nulladik bitbe, még akkor is, ha forráskód szintjén ez a Proxy objektumok értékadásának látszik.

A problémát azért oldja meg mégis a delete-es sor (valószínűleg nem szándékosan), mert az nem csak azt fejezi ki a fordítónak, hogy nem hívható az értékadó operátor, hanem azt is, hogy a paraméterezése a szokásostól eltérő. Észre kell venni, hogy ebben a sorban nincs const szó; a paraméter Proxy &other nem konstans referencia. Vagyis ez így két dolgot jelent: a) ennek az osztálynak nem konstans objektumot vár az értékadó operátora, b) és mellesleg nem használható.

A bitarray[0] = bitarray[1] sorban két Proxy keletkezik, és mindkettő temporális objektum. A referencia típusa a jobb oldali, bitarray[1] temporális objektum miatt lényeges; a nem konstans referencia nem vehet át temporális objektumot paraméterként. Ezért a fordító számításba sem veszi azt az operátort, amikor ezt az értékadást megpróbálja feloldani. Helyette konverziót keres, és rátalál az operator bool + operator=(bool) párosra, ami pedig épp azt csinálja, amit kell. A delete-elésnek nincs is jelentősége, működne anélkül is, egy kósza deklarációval.

Telítéses aritmetika

Írj osztályt, amely telítéses aritmetikát (saturation arithmetic) valósít meg!

Ebben a tárolt számok értékének van egy minimális és maximális értéke. Túlcsordulás (vagy alulcsordulás) esetén a határon a kapott érték sosem lép túl. Például ha az alsó határ 0, akkor 10-5 = 5, de 5-7 = 0. Vagy ha a felső határ 255, akkor 250 + 7 = 255.

A digitális jelfeldolgozásban (pl. hangfeldolgozásban) használt eszközök működnek így.

Számrendszerek

Nézd meg az extrákban az I/O manipulátorokkal kapcsolatos írást! Ez az előadás mértékegységes példájának folytatása. Utána...

a) Írj bináris szám kiíró I/O manipulátort! A használata legyen az alábbi:

std::cout << bin << 0 << std::endl;         /* 0 */
std::cout << bin << 240 << std::endl;       /* 11110000 */

Vajon mi lehet itt a bin típusa?

b) Írj akárhányas számrendszerben kiíró I/O manipulátort!

std::cout << base(2) << 240 << std::endl;   /* 11110000 */
std::cout << base(7) << 240 << std::endl;   /* 462 */
std::cout << base(16) << 240 << std::endl;  /* F0 */

Melyik az első részkifejezés, amit ki kell értékelnie a fordítónak? Indulj ki ebből!

Ügyelj arra, hogy a megvalósításban ne legyen pow() (egész számokkal kell dolgozni, a pow() pedig lebegőpontos számokkal dolgozik, pontatlanság lehet). Ezen kívül, ne legyen std::to_string() se! Legalábbis egyetlen számjegy sztringgé alakítására semmiképp, mivel akkor minden számjegyhez külön std::string objektum jönne létre.

Mértékegységek

Folytasd az előadás mértékegységes programját!

  1. Mekkora a Ferrari motorjának átlagos teljesítménye a programban megadott gyorsítás közben? (A mozgási energiát az E=1/2*m*v2 képletből tudod kiszámolni; ennyi energiát adott le a motor 4 másodperc alatt.)

  2. A mértékegység osztály dimenzió nélküli változata (m0*kg0*s0) egy egyszerű valós számot ad meg, amely általában egy arányt jelképez. Például a hossz/hossz egy dimenzió nélküli számot ad. Specializáld úgy az osztályt, hogy a dimenzió nélküli mértékegységek konvertálhatók legyenek valós számmá, illetve automatikusan létrehozhatók legyenek valós számból!

  3. Az előző feladat megvalósítása után, működnek-e automatikusan a valós*dimenziónélküli (pl. 2.0*(v1/v2)), és a valós+dimenziónélküli (pl. 1.0+(v1/v2)) alakú kifejezések? Miért?

approx()

A pytest API tartalmaz egy approx() nevű függvényt, amelyikkel valós számok „nagyjából” egyenlőségre vizsgálhatóak:

0.1 + 0.2 == approx(0.3)

Oldd meg, hogy C++-ban is lehessen ilyet írni!

2. Literálisok

constexpr minősítésű _binary

Meg lehet oldani, hogy a _binary utótag operátor constexpr legyen? Ehhez az kell, hogy az utótag operátor függvénye egyetlen return utasításból álljon. (Egy ügyesen megírt rekurzív segédfüggvénnyel próbálkozz!)

Megoldás

Az alábbi, binary_val függvény két részre bontja az átalakítandó számot. A szám már átalakított, bal oldali része a left, a maradék, át még nem alakított része a right paraméterben van. Pl. az 1101 számnál az első számjegy átalakítása után left=1, right="101" paraméterekkel hívódik a függvény. Egy lépésben egy számjegy átalakítása történik meg, ehhez a left-et eggyel balra kell léptetni, és beírni a right következő számjegyét: left=11, right="01" értékekkel hívni újra a függvényt. Ha right üres, nincs teendő.

constexpr unsigned long long binary_val(unsigned long long left, char const *right) {
    return right[0] == '\0'
           ? left
           : binary_val(left<<1 | (right[0]-'0'), right+1);
}

constexpr unsigned long long operator "" _binary(char const *str) {
    return binary_val(0, str);
}

A binary_val() törzse egy return utasításból áll, ezért constexpr lehet. Az operator "" _binary() törzse egy return utasításból áll, és csak constexpr függvényt hív, ezért ez is lehet constexpr.

Dátum literálisok I.

Date d = "2013.08.11"_date;
std::cout << d << std::endl;                    /* 2013.08.11. */
std::cout << "'99.08.11"_date << std::endl;     /* 1999.08.11. */
std::cout << "'12.08.11"_date << std::endl;     /* 2012.08.11. */
std::cout << "08.11.2013"_date << std::endl;    /* 2013.08.11. */

Dátum literálisok II.

A feladat ugyanaz, mint az előbb – de fordítási időben kell megoldani, constexpr függvényekkel.

Ha minden rendben van, akkor egy helyes megoldás esetén lehet "'99.08.11"_date.getYear() méretű globális tömböt létrehozni, vagy ezt a számot (1999) sablonparaméternek használni. Ezek persze értelmetlen példák, csak ötletként szerepelnek itt az ellenőrzésre, hogy tényleg fordítási időben fut-e a dátum értelmezése. A lényeg az, hogy mivel literálisról van szó, azt már fordítás közben is lehet (és érdemes) értelmezni, mert úgysem változik futás közben.