Típusok használata

Czirkos Zoltán · 2024.08.20.

Típusok, I/O manipulátorok, operátorok átdefiniálása.

Gyakorlófeladatok a típusokról szóló előadáshoz. A lényeg mindenhol, hogy olyan típusokat hozzunk létre, olyan operátorokat adjunk meg, amelyekkel aztán szintaktikailag világos, könnyen érthető programokat tudunk írni.

Feltöltés

A laborhoz kiírt beadandó a szokásos helyen van, az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.

Labor otthoni munkában

A labor teljesítéséhez mind a négy feladatot meg kell oldani.

1. A TimeInterval osztály

TimeInterval i1{65};
std::cout << i1 << std::endl;           // 1h 5m

std::cout << (5_h + 79_m) << std::endl; // 6h 19m
Megoldás

Egyszerűbb az összeadás, ha belül csak egy int tárolódik; az osztály mindig csak percekkel dolgozik.

#include <iostream>
 
class TimeInterval {
  public:
    explicit TimeInterval(int interval) : interval_{interval} {}
    TimeInterval operator+ (TimeInterval rhs) const {
        return TimeInterval(this->interval_ + rhs.interval_);
    }
    int get_hour() const { return interval_ / 60; }
    int get_min() const { return interval_ % 60; }
  private:
    int interval_;
};
 
std::ostream & operator<< (std::ostream & os, TimeInterval t) {
    os << t.get_hour() << "h " << t.get_min() << "m";
    return os;
}
 
TimeInterval operator"" _h (unsigned long long h) {
    return TimeInterval(h * 60);
}
 
TimeInterval operator"" _m (unsigned long long m) {
    return TimeInterval(m);
}
 
int main() {
    TimeInterval i1(65);
    std::cout << i1 << std::endl;           // 1h 5m
    
    std::cout << (5_h + 79_m) << std::endl; // 6h 19m
}

2. Sztring konverzió

C++14-ben már létezik std::string-gé alakító operátor: a "hello"s kifejezés típusa std::string. Írj egy ugyanígy működő _s operátort! Ha mindent jól csinálsz:

std::cout << "hello"_s + " vilag" << std::endl;         /* hello vilag */
std::cout << "hello\0vilag"_s.length() << std::endl;    /* 11 */

Vajon lehet ez az _s operátor constexpr? Miért?

Megoldás

Olyan operátort kell írni, amely paraméterként átveszi a sztring méretét is. Egyrész különbözteti meg az "123"_xyz és az 123_xyz operátort (overload az _xyz operátorra), másrészt pedig így lehet lezáró nulla is a sztringben. Ez kell a második sorhoz.

#include <iostream>
#include <string>

std::string operator"" _s (char const *str, size_t siz) {
    return std::string(str, str + siz);
}

int main() {
    std::cout << "hello"_s + " vilag" << std::endl;         /* hello vilag */
    std::cout << "hello\0vilag"_s.length() << std::endl;    /* 11 */
}

Nem lehet constexpr, mert az std::string dinamikus memóriakezelést használ.

3. Bináris literális utótag operátor

Írj bináris utótag operátort! Milyen formában érdemes átvennie ennek az értéket, nyers (raw), azaz sztring, vagy szám (cooked) formában?

int main() {
    std::cout << 1111_binary << std::endl;      /* 15 */
    std::cout << 10000000_binary << std::endl;  /* 128 */
    std::cout << 10110111010111110111_binary << std::endl;  /* 751095 */
}
Tipp

Az átalakításhoz használj Horner-elrendezést! Akkor nem kell tudni előre, hány számjegy lesz. Tízes számrendszeren bemutatva:

12345 = ((((1)·10 + 2)·10 + 3)·10 + 4)·10 + 5

Megoldás

Sztringként átvéve egyszerű a megvalósítás, akár indexelni is tudjuk a számjegyeket. Nem jó ötlet feldolgozott formában átvenni a karaktereket, mert akkor a fordító tízes számrendszerben értelmezi a számot, és könnyen túlcsordult értéket kaphatunk: binárisan egy szám sok számjegyből áll.

unsigned long long operator"" _binary(char const *str) {
    unsigned long long result = 0;
    for (size_t i = 0; str[i] != '\0'; ++i)
        result = result<<1 | (str[i]=='1' ? 1 : 0);
    return result;
}

Egyébként a bináris literális a C++14-be ez bekerült, a hexa 0x prefix mintájára 0b prefixként.

4. Operátorok, konverziók: az okos Float

A lebegőpontos (float, double) értékeken végzett számítások nem pontosak. Vannak számok, amelyeket a kettes alapú normálalakban nem lehet ábrázolni. Emiatt két float vagy double egyenlőségét nem szabad vizsgálni, mert váratlan eredményt adhat.

#include <iostream>
#include <iomanip>

int main() {
    std::cout << std::setprecision(20) << 9.95; // 9.9499999999999992895
}

A feladatod egy olyan Float osztályt írni, amely figyelembe veszi a pontatlanságokat az összehasonlításoknál. Az „egyenlő-e” legyen „ε sugarú körön belül van”, a „nagyobb-e” legyen „legalább ε-nyival nagyobb” és így tovább. Minden egyéb viselkedésében egyezzen meg egy Float objektum a beépített float-tal!

Float f1 = 1.0f, f2 = 1.00001f;
std::cout << (f1 == f2);    /* igaz */
  1. Rajzolj fel egy számegyenest papíron, és jelölj be rajta egy f számot. Rajzold be, hogy mely tartományokat kell f-nél kisebbnek, egyenlőnek, és nagyobbnak tekinteni. Ebből az ábrából dolgozz!

    Megoldás
  2. Írd meg a Float osztályt! Maga az osztály minél kisebb legyen, törekedj arra, hogy később inkább globális függvényeket használj az operátorok megvalósításához. Milyen konverziókat kell definiálni a Float osztályhoz? Melyiknek ajánlatos explicitnek lennie, és miért? Írd meg egyelőre csak a kisebb < operátort! ε=10-4. Ennek a kódrészletnek kell működnie:

    Float f1 = 1.0f,
          f2 = 1.00001f,
          f3 = 100;
    
    std::cout << (f1 < f2) << std::endl;    /* hamis */
    std::cout << (f1 < f3) << std::endl;    /* igaz */
    Megoldás

    A konstruktor ne legyen explicit, hogy bármilyen beépített valós érték az okos párjává konvertálódhasson. Az operator float viszont jobb, ha explicit, hogy külön ki kelljen írni a konverziót, amikor vissza akarjuk butítani az értéket.

  3. Írd meg az összeadás, kivonás operátorokat! Írd meg a += és -= operátorokat úgy, hogy visszavezeted őket az előzőekre! Írd meg a kiírás << operátort is! Ha ezek készen vannak, az alábbi kódrészlet is működni fog. Ellenőrizd az eredményt!

    f1 = f2 + f3;
    
    for (Float f = 0.999; f < 1.001; f += 0.0001) {
        std::cout << f << '\t' << (f < 1.0) << std::endl;
    }
  4. Az ε legyen egy konstans változóban tárolva. Hol érdemes tárolni ezt a változót? Ha eddig nem változóként adtad meg, akkor add meg úgy, és javítsd ki a < operátort!

    Megoldás

    A Float osztályban, statikus adattagként. A constexpr fordítási időben kiértékelhető konstanst jelent. Statikus adattag inicializálható osztályon belül is, ha megkapja a constexpr jelzőt.

    class Float {
        static constexpr float epsilon = 1e-4f;
    };
  5. Írd meg a többi összehasonlító operátort is! Összesen öt függvény kell, de mindegyik egysoros: >, <=, >=, ==, !=. Vezesd vissza mindet a < operátorra! Teszteld őket a fenti ciklussal!

  6. Írd meg az egyoperandusú + és - operátorokat, hogy ilyet lehessen írni:

    std::cout << -f1;
  7. Van-e futási idejű költsége a Float osztálynak a float-okhoz képest?

    Megoldás

    Memóriafoglalásban: sizeof(float)==sizeof(Float), tehát nincs.

    Sebességben: az összehasonlítás legyen fair! Ha egy olyan változathoz hasonlítjuk a programot, ahol kézzel mindig kiírtuk az ε sugarú összehasonlítást, akkor nincs. Használjunk optimalizálást és inline függvényeket!

Megoldás
#include <iostream>
#include <cmath>


class Float {
  public:
    Float() = default;
    Float(float value) : value_(value) {}
    explicit operator float() const { return value_; }

    static constexpr float epsilon = 1e-4f;

  private:
    float value_;
};


Float operator+(Float f1, Float f2) {
    return float(f1) + float(f2);
}

Float & operator+=(Float &f1, Float f2) {
    return f1 = f1 + f2;
}

Float operator-(Float f1, Float f2) {
    return float(f1) - float(f2);
}

Float & operator-=(Float &f1, Float f2) {
    return f1 = f1 - f2;
}

/* egyoperandusú */
Float operator-(Float f) {
    return -float(f);
}

/* kisebb */
bool operator<(Float f1, Float f2) {
    return float(f1) < float(f2)-Float::epsilon;
}

/* nagyobb: "kisebb" fordítva */
bool operator>(Float f1, Float f2) {
    return f2<f1;
}

/* nagyobb vagy egyenlő: nem kisebb */
bool operator>=(Float f1, Float f2) {
    return !(f1<f2);
}

/* kisebb vagy egyenlő: nem nagyobb */
bool operator<=(Float f1, Float f2) {
    return !(f1>f2);
}

/* nem egyenlő: kisebb vagy nagyobb */
bool operator!=(Float f1, Float f2) {
    return f1 > f2 || f1 < f2;
}

/* egyenlő: nem nem egyenlő */
bool operator==(Float f1, Float f2) {
    return !(f1 != f2);
}

/* kíirás */
std::ostream & operator<< (std::ostream & os, Float f) {
    return os << float(f);
}

/* beolvasás */
std::istream & operator>> (std::istream & is, Float & f) {
    float raw_f;
    is >> raw_f;
    f = raw_f;
    return is;
}


int main() {
    Float f1 = 1.0f,
          f2 = 1.00001f,
          f3 = 100;
    
    std::cout << (f1 < f2) << std::endl;    /* hamis */
    std::cout << (f1 < f3) << std::endl;    /* igaz */

    f1 = f2+=f3;

    for (Float f = 0.999; f < 1.001; f += 0.0001) {
        std::cout << f << '\t' << (f < 1.0) << std::endl;
    }
    
    std::cout << -f1;
}

5. További feladatok