Sablon metaprogramozás

Czirkos Zoltán · 2023.08.21.

Stratégia és típusinformációs osztályok írása.

A laborokhoz

A laborok mellé minden héten lesz kiírva egy beadandó 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 legalább az első két feladatot meg kell oldani.

Javaslat a munkához: sablon kódot kell írni. Számíts arra, hogy rengeteg lesz hiba esetén a hibaüzenet, és nem lesznek túl informatívak! Ne találgass-tippelgess... Legyél szuper figyelmes a szintaxissal kapcsolatban!

1. Típusinformáció: Sztring == sZtRIng

Adott az alábbi sztring osztály, amely most az egyszerűség kedvéért nem használ dinamikus memóriakezelést:

class String {
  private:
    char data[100];

  public:
    String() {
        data[0] = 0;
    }

    String(char const *init) {
        strcpy(data, init);
    }
    
    bool operator==(String const &rhs) {
        String const &lhs = *this;
        size_t i;
        for (i = 0; lhs.data[i] != 0 && rhs.data[i] != 0;  ++i)
            if (lhs.data[i] != rhs.data[i])
                return false;
        return lhs.data[i] == 0 && rhs.data[i] == 0;
    }
};


int main() {
    String s1 = "hello", s2 = "HeLLo";
    std::cout << (s1 == s2);
}

Ezen a sztring osztályon több dolgot is lehetne általánosítani. Például a karakter típusa az egybájtos char-on túl lehetne másféle is, pl. char16_t vagy char32_t az Unicode kódolású szövegek tárolásához. Továbbá, az összehasonlítást nem feltétlenül kellene az == operátorral végezni. Lehetne pl. úgy is, hogy figyelmen kívül hagyjuk a kisbetű–nagybetű különbségeket.

Ezek sablonparaméterek lehetnének. Hogy ne legyen túl sok sablonparamétere a String osztálynak, érdemes ezeket egy külön osztályba tenni. Például a fenti viselkedés leírásához a CharTraits osztályban kell lennie egy belső típusnak, és egy statikus függvénynek, CharTraits::type és CharTraits::equal():

struct CharTraits {
    using Type = char;
    
    static bool equal(char c1, char c2) { return c1 == c2; }
};

Feladatok

  • Írd át úgy a fenti String osztályt, hogy a sablonparamétere egy ilyen viselkedésosztály legyen! Használják az osztály függvényei a sablonparaméterből vett típust és az onnan vett karakterösszehasonlító függvényt! Figyelj arra, hogy ehhez a strcpy() függvényhívást is át kell írnod, mert az csak char*-okon működik.
  • Példányosítsd az új String sablonod a fenti CharTraits osztállyal, és teszteld a működését!
  • Hozz létre egy olyan karakterosztályt is, amelynek karakterei érzéketlenek a kisbetű–nagybetű különbségre! Teszteld ezt is!

Mi történik, ha összehasonlítasz az == operátorral két sztringet, amelyek karakterosztálya eltérő? Miért?

Tipp

Ha sablon osztályból kell egy belső típust hivatkozni, elé kell írni, hogy typename:

template <typename SABLONOSZTALY>   // sablon kódon belül vagyunk
void f() {
    typename SABLONOSZTALY::BelsoTipus x;
}

Ennek okairól itt olvashatsz bővebben.

Megoldás
#include <iostream>


template <typename CHAR_TRAITS>
class String {
  private:
    using CharType = typename CHAR_TRAITS::Type;
    CharType data[100];

  public:
    String() {
        data[0] = 0;
    } 

    String(CharType const *init) {
        size_t i;
        for (i = 0; init[i] != 0; ++i)
            data[i] = init[i];
        data[i] = 0;
    }
    
    bool operator==(String const &rhs) {
        String const &lhs = *this;
        size_t i;
        for (i = 0; lhs.data[i] != 0 && rhs.data[i] != 0;  ++i)
            if (!CHAR_TRAITS::equal(lhs.data[i], rhs.data[i]))
                return false;
        return lhs.data[i] == 0 && rhs.data[i] == 0;
    }
};


struct CharTraits {
    using Type = char;
    
    static bool equal(char c1, char c2) {
        return c1 == c2;
    }
};


struct CharTraitsCaseInsensitive: public CharTraits {
    static bool equal(char c1, char c2) {
        return toupper(c1) == toupper(c2);
    }
};


int main() {
    String<CharTraits> s1 = "hello", s2 = "hEllo";
    std::cout << (s1 == s2) << std::endl;

    String<CharTraitsCaseInsensitive> s3 = "hello", s4 = "hEllo";
    std::cout << (s3 == s4) << std::endl;
}

A CharTraitsCaseInsensitive osztály származtatható a CharTraits osztályból, így megörökli a Type belső típust. Ennek a származtatásnak akkor jönne ki igazán az előnye, ha több belső függvény lenne, mert csak egyet szeretnénk felülírni. Bár az equal() függvény statikus, és így nem lehet virtuális, mégis felüldefiniálható a leszármazásban, mert a sablonoknál fordítási időben a megfelelő típus látszik!

Az eltérő viselkedésosztályból példányosított sztring objektumok nem hasonlíthatók össze, mert különböző típusúnak számítanak. Ez az összehasonlításnál nem is baj, amúgy sem lehet eldönteni, hogy egy kisbetű–nagybetűre érzéketlen és egy arra érzékeny sztringet hogyan kellene összehasonlítani.

Az std::basic_string és az std::char_traits ugyanígy működnek; std::string == std::basic_string<char, std::char_traits<char>>, illetve std::char_traits<char>::char_type és std::char_traits<char>::eq().

Szorgalmi feladat: Nézz utána a szabványos std::basic_string, std::basic_ostream, és az std::char_traits osztályoknak, és hasonlítsd össze őket a most megírt programoddal! Ez a feladat a GotW #29 alapján készült.

2. Stratégiák: vektor túlindexelése

Adott az alábbi vektor osztály.

#include <iostream>
#include <string>

template <typename T>
class Vector {
  private:
    size_t size;
    T* data;
    
  public:
    explicit Vector(size_t size)
      : size{size}
      , data{new T[size]} {
    }
    Vector(Vector const &) = delete; /* lusta */
    Vector& operator=(Vector const &) = delete; /* lusta */
    ~Vector() {
        delete[] data;
    }
    
    T& operator[](size_t idx) {
        return data[idx];
    }
};

int main() {
    Vector<std::string> v1(10);
}

A vektor túlindexelésekor többféle dolog történhet:

  • Nem foglalkozik vele a vektor; a sebesség érdekében a hívóra bízza a helyes indexelést.
  • Kivételt dob.
  • Körkörösen visszatér a vektor elejére (pl. 100 elemű esetén v[100] = v[0], t[101] = t[1] stb.)
  • Megnyújtja a vektort akkorára, hogy helyessé váljon az indexelés.

Írd át a vektor osztályt úgy, hogy sablonparamétere legyen a túlindexelést kezelő stratégia osztály is! Érdemes elsőként a vektor indexelő operátorát átalakítani, ott látod leginkább, hogy mit vársz a stratégia osztálytól (milyen interfésszel kell rendelkeznie). A stratégiának statikus függvénye kell megkapja az ellenőrizendő indexet és az aktuális méretet (0 ≤ index < méret). Írd meg ilyen módon az üres stratégiát és a kivételt dobó stratégiát! Próbáld ki mindkettőt!

Ha ezek készen vannak, akkor írd a vektort átméretező stratégiát is! Ehhez módosítani kell a stratégia paraméterezését. Annak látnia kell a vektort is, amely meghívja az indexelést ellenőrző függvényt – azt kell majd átméreteznie. Legegyszerűbb ezt úgy implementálni, hogy a stratégia osztályokon belül a statikus függvény egy sablon (nem az osztály, csak a függvény!), mert a különféle típusokkal és stratégiákkal példányosított vektorokat át kell tudnia venni paraméterként.

Ügyelj arra, hogy az utóbbi feladatnál nem túl jó megoldás, ha a tényleges memóriakezelés a stratégia osztályba kerül. Annak ez egyáltalán nem feladata.

Megoldás

A vektor paraméterként átadását legegyszerűbb úgy kezelni, ha a stratégiák ellenőrző függvényei sablonok. Erre azért van szükség, mert a vektorokból többféle lehet. Egy egyszerű implementáció:

#include <iostream>
#include <stdexcept>
#include <algorithm>
#include <string>


template <typename T, typename RangePolicy>
class Vector {
  private:
    size_t size;
    T* data;
    
  public:
    explicit Vector(size_t size)
      : size{size}
      , data{new T[size]} {
    }
    Vector(Vector const &) = delete; /* lusta */
    Vector& operator=(Vector const &) = delete; /* lusta */
    ~Vector() {
        delete[] data;
    }
    
    T& operator[](size_t idx) {
        std::clog << "index " << idx << std::endl;
        RangePolicy::check_index(*this, idx);
        return data[idx];
    }
    
    void resize(size_t newsize) {
        std::clog << "resize to " << newsize << std::endl;
        T* newdata = new T[newsize];
        std::copy(data, data+std::min(size, newsize), newdata);
        delete[] data;
        data = newdata;
        size = newsize;
    }
    size_t getsize() const {
        return size;
    }
};


struct RangePolicyDontCare {
    template <typename V>
    static void check_index(V& vec, size_t idx) {
        std::clog << "don't care" << std::endl;
    }
};


struct RangePolicyThrowException {
    template <typename V>
    static void check_index(V& vec, size_t idx) {
        if (idx >= vec.getsize())
            throw std::range_error("index out of bounds");
    }
};

struct RangePolicyResizeIfNeeded {
    template <typename V>
    static void check_index(V& vec, size_t idx) {
        if (idx >= vec.getsize()) {
            vec.resize(idx+1);
        }
    }
};


int main() {
    Vector<std::string, RangePolicyDontCare> v1(10);
    v1[100];
    
    try {
        Vector<std::string, RangePolicyThrowException> v2(10);
        v2[100];
    } catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
    
    Vector<std::string, RangePolicyResizeIfNeeded> v3(10);
    v3[100] = "alma";
    std::cout << "size is " << v3.getsize() << std::endl;
    std::cout << v3[100] << std::endl;
}

Szorgalmi feladat: implementálj olyan stratégia osztályt, egy adaptert, amelyik kettő másik stratégia osztályt vár sablonparaméterként, és az azokba foglalt tevékenységeket egymás után elvégzi! Írj olyan stratégia osztályt, amelyik naplózza a túlindexelést! A naplózó és az átméretező stratégiát kombináld az adapter osztállyal, és hozz létre így egy olyan vektort, amelyik jelzi a naplófájlban, ha átméretezésre volt szükség!

Megoldás
struct RangePolicyLog {
    template <typename V>
    static void check_index(V& vec, size_t idx) {
        if (idx >= vec.getsize())
            std::clog << "index out of bounds, size "
                      << vec.getsize() << ", idx " << idx << std::endl;
    }
};

template <typename P1, typename P2>
struct RangePolicyComposite {
    template <typename V>
    static void check_index(V& vec, size_t idx) {
        P1::check_index(vec, idx);
        P2::check_index(vec, idx);
    }
};

using RangePolicyLogAndResize = RangePolicyComposite<RangePolicyLog, RangePolicyResizeIfNeeded>;

int main() {
    Vector<int, RangePolicyLogAndResize> v1(10);
    v1[100];
}

3. Típusinformáció: Leszármazottja-e?

Írj olyan osztálysablont, amelynek két sablonparamétere van: egy BASE és egy DERIVED típus! Legyen ebben az osztályban egy statikus, fordítási idejű konstans változó, amelynek értéke true, ha a DERIVED paraméter tényleg leszármazottja a BASE-nek, amúgy pedig false!

class Base {};
class Derived: public Base {};

int main() {
    std::cout << IsDerived<Base, Derived>::value;    // 1
    std::cout << IsDerived<Base, std::ostream>::value;    // 0
}

Ezt arra alapozva tudod megírni, hogy egy DERIVED* típusú mutatóval meghívva egy BASE* és void* típusú paraméterekkel hívható függvényt, az előbbit fogja választani a fordító, mert speciálisabb.

Tipp

Emlékezz vissza: tetszőleges típusú pointer előállítható egy null értékű pointer konverziójával.

Megoldás

Egy egyszerű megoldás, C++11 constexpr függvényekkel:

template <typename BASE, typename DERIVED>
class IsDerived {
  private:
    static constexpr bool is_derived_helper(BASE *) {
        return true;
    }
    static constexpr bool is_derived_helper(void *) {
        return false;
    }
  public:
    static constexpr bool value = is_derived_helper(static_cast<DERIVED*>(nullptr));
};

4. Érték vagy referencia?

Azt szoktuk mondani, hogy az érték paraméterű függvényekkel szemben a konstans referencia paraméterű függvényeket preferáljuk: ne másoljuk le az objektumot, ha nem muszáj, mert a felesleges másolások lassítják a programot. No igen, de ez csak nagyobb objektumokra igaz; az alábbi függvény éppen a referenciák miatt lassú. Mert ahelyett, hogy átadnánk a számok értékét (valószínűleg a processzor egy regiszterében), címeket adunk át, és fölösleges memóriaolvasási műveletekre kényszerítjük a gépet:

int const& greater(int const& a, int const& b) {
    return a > b ? a : b;
}

Beépített típusok esetén az érték szerinti paraméterátadás a gyorsabb:

int greater(int a, int b) {
    return a > b ? a : b;
}

A feladat: írj két sablonfüggvényt, amelyek tetszőleges, T típusú objektumok közül a nagyobbikkal térnek vissza! Az egyik dolgozzon referenciákkal, a másik értékekkel! A type_traits fejlécfájl segédosztályaival oldd meg, hogy beépített típusokkal való példányosítás esetén az utóbbi, osztályokkal való példányosítás esetén az előbbi hívódjon!

Vissza kellett olvasnod a szövegben, hogy melyik az „előbbi” és melyik az „utóbbi”? Ha az lenne odaírva, hogy „beépített típusoknál érték szerint, osztályoknál cím szerinti paraméterátadás legyen”, akkor nem kellett volna. Ugyanígy időbe telik a gépnek is egy referencia feloldása: extra memóriaművelet.

Megoldás
#include <type_traits>
#include <iostream>
#include <string>


template <typename T,
          typename = typename std::enable_if<!std::is_class<T>::value>::type>
T greater(T a, T b) {
    std::cout << "value\n";
    return a > b ? a : b;
}

template <typename T,
          typename = typename std::enable_if<std::is_class<T>::value>::type>
T const& greater(T const& a, T const& b) {
    std::cout << "const ref\n";
    return a > b ? a : b;
}


int main() {
    std::cout << greater(3, 4) << std::endl;
    std::cout << greater(std::string{"alma"}, std::string{"korte"}) << std::endl;
}

5. Mit csinál a függvény?

Adott az alábbi kódrészlet. Ebben a feladatban ezt a függvénypárost felhasználva kell dolgozni.

template <typename T, decltype(std::begin(*static_cast<T *>(nullptr))) * = nullptr>
constexpr bool mystery_func(T const *) {
    return true;
}


constexpr bool mystery_func(void const *) {
    return false;
}

Feladatok

  • Vajon mire jók a függvények? Hogyan kell őket használni, és mit mutat meg a visszatérési értékük?
  • Tedd be a függvényeket egy parametrizálható segédosztályba, amelynek statikus value adattagja megmondja, hogy a példányosító típusa rendelkezik-e azzal a bizonyos tulajdonsággal vagy nem, amelyet ezek a függvények tesztelnek.
  • Írj egy print() függvénysablont, amely tetszőleges típusú paramétert átvehet (konstans referenciával), és annak kiíró operátorát << használva kiírja a tartalmát az std::cout-ra!
  • Az std::enable_if segédosztály használatával specializáld ezt a print() függvénysablont arra az esetre, amikor a példányosító típus rendelkezik a mystery_func() által tesztelt tulajdonsággal, és arra, amikor nem!
  • Működik tömbre is az így megírt függvényed? Miért?
  • Mi a különbség az alábbi deklarációk között?
    template <typename T, decltype(std::begin(*static_cast<T *>(nullptr))) * = nullptr>
        bool mystery_func(T const *);
    
    template <typename T, typename = decltype(std::begin(*static_cast<T *>(nullptr)))>
        bool mystery_func(T const *);
    
    template <typename T, size_t = sizeof(std::begin(*static_cast<T *>(nullptr)))>
        bool mystery_func(T const *);
Megoldás

A függvények azt tesztelik, hogy a nekik adott típusú objektumnak van-e iterátora, vagy nincs. Ezt a sablonfüggvény úgy éri el, hogy az std::begin függvénynek átad egy képzeletbeli objektumot. Ha az std::begin() nem paraméterezhető azzal a típussal, a SFINAE szabály miatt a deklarációt a fordító eldobja. A null értékű pointer dereferálásával nincs gond, mivel a decltype() belseje kiértékeletlen környezet. Mindez tömbre is működik, mert az std::begin-nek létezik a tömbökre specializált változata.

A három deklaráció között semmilyen érdemi különbség nincs, mindegyik észrevétlen és névtelen sablonparamétert hoz létre, és mindegyik aktiválja a SFINAE szabályt.

#include <iostream>
#include <type_traits>


template <typename TYPE>
class HasIterator {
  private:
    template <typename T, decltype(std::begin(*static_cast<T *>(nullptr))) * = nullptr>
    static constexpr bool has_iterator(T const *) {
        return true;
    }

    static constexpr bool has_iterator(void const *) {
        return false;
    }
  
  public:
    static constexpr bool value = has_iterator(static_cast<TYPE *>(nullptr));
};


template <typename T, typename std::enable_if<!HasIterator<T>::value>::type * = nullptr>
void print(T const & what) {
    std::cout << what;
}


template <typename T, typename std::enable_if<HasIterator<T>::value>::type * = nullptr>
void print(T const & what) {
    std::cout << "{";
    for (auto const & elem : what) {
        print(elem);
        std::cout << ", ";
    }
    std::cout << "}";
}


int main() {
    int i = 2;
    int a[] = { 1, 2, 3 };
    print(i);
    print(a);
}

6. Irodalom

  1. Herb Sutter: Strings (GotW #29).