12. hét: Sablon metaprogramozás

Czirkos Zoltán · 2022.06.21.

A félév időbeosztásától függően a metaprogramozás témakörre egy vagy két labor szokott jutni. Ezért előfordulhat, hogy itt a laborfeladatok is szerepelnek.

1. Trait osztályok

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. Azért, 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; }
};
  • Í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 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?

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.

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().

2. Típusok szerinti esetszétválasztás

print()

A feladat egy olyat print() függvénycsaládot írni, amelyik meg tudja különböztetni, más formátumban írja ki a számokat, a karaktereket és a sztringeket.

  • A számokat (int, double, egyebek) önmagukban, pl. 5 vagy 5.1.
  • A karaktereket (pl. char vagy char16_t) aposztrófok között: 'a'.
  • A sztringeket (pl. char* vagy std::string) pedig idézőjelek között: "hello".
  • Egyéb típusokat ne lehessen kiírni vele.

A feladatot sablon metaprogramozással oldd meg!

Útmutatás. Elvileg csak három print() függvényre lesz szükséged, amelyek közül mindig valamelyiket az std::enable_if engedélyezi, a többit tiltja. Nézz szét a type_traits dokumentációjában, milyen sablonokat tudsz felhasználni és miket kell magadnak megírnod. Figyelned kell arra is, hogy T és T const eltérő típusok, ugyanakkor vissza lehet vezetni egyiket a másikra. És arra, hogy ne zsúfolj mindent a print()-ek fejlécébe; ha kell, írj segédfüggvényeket (metafüggvényeket)!

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

template <typename T> struct my_is_char { static constexpr bool value = false; };
template <> struct my_is_char<char> { static constexpr bool value = true; };
template <> struct my_is_char<signed char> { static constexpr bool value = true; };
template <> struct my_is_char<unsigned char> { static constexpr bool value = true; };
template <> struct my_is_char<char16_t> { static constexpr bool value = true; };
template <> struct my_is_char<char32_t> { static constexpr bool value = true; };
template <> struct my_is_char<wchar_t> { static constexpr bool value = true; };

template <typename T> struct my_is_string { static constexpr bool value = false; };
template <typename T> struct my_is_string<std::basic_string<T>> { static constexpr bool value = true; };
template <typename T> struct my_is_string<T*> { static constexpr bool value = my_is_char<typename std::remove_cv<T>::type>::value; };


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

template <typename T>
void print(T what, typename std::enable_if<my_is_char<T>::value>::type * = nullptr) {
    std::cout << '\'' << what << '\'' << std::endl;
}

template <typename T>
void print(T what, typename std::enable_if<my_is_string<T>::value>::type * = nullptr) {
    std::cout << '\"' << what << '\"' << std::endl;
}

int main() {
    print(5.1);
    print('a');
    print(std::string("hello"));
}

advance()

Az std::advance függvény n lépéssel léptet előre egy iterátort:

auto it = valami_tarolo.begin();
std::advance(it, 5);

Ez annyiban okosabb egy n-szer futó, ++it törzsű ciklusnál, hogy véletlen elérésű iterátorok esetén automatikusan, fordítási időben it+=n-né változik.

Írd meg a saját advance() függvényedet dispatcher függvény segítségével!

Megoldás
#include <iterator>
#include <vector>
#include <list>
#include <iostream>

template <typename ITER>
void my_advance(ITER & it, int n, std::random_access_iterator_tag) {
    std::cout << "O(1) :)\n";
    it += n;
}

template <typename ITER>
void my_advance(ITER & it, int n, std::forward_iterator_tag) {
    std::cout << "O(n) :(\n";
    for (int i = 0; i < n; ++i)
        ++it;
}

template <typename ITER>
void my_advance(ITER & it, int n) {
    using tag = typename std::iterator_traits<ITER>::iterator_category;
    my_advance(it, n, tag());
}

int main() {
    std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
    auto iv = v.begin();
    my_advance(iv, 5);

    std::list<int> l = { 1, 2, 3, 4, 5, 6, 7 };
    auto il = l.begin();
    my_advance(il, 5);
}

C++17: írd meg a saját advance() függvényedet if constexpr segítségével! Vajon jobb vagy rosszabb ez a megoldás, mint a fenti?

Megoldás
#include <iterator>
#include <vector>
#include <list>
#include <iostream>

template <typename ITER>
void my_advance(ITER & it, int n) {
    using tag = typename std::iterator_traits<ITER>::iterator_category;
    if constexpr (std::is_base_of<std::random_access_iterator_tag, tag>::value) {
        std::cout << "O(1) :)\n";
        it += n;
    } else {
        std::cout << "O(n) :(\n";
        for (int i = 0; i < n; ++i)
            ++it;
    }
}

int main() {
    std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
    auto iv = v.begin();
    my_advance(iv, 5);

    std::list<int> l = { 1, 2, 3, 4, 5, 6, 7 };
    auto il = l.begin();
    my_advance(il, 5);
}

A feladattal kapcsolatban lásd még a Concepts előadás ide vágó részét! (Pár héttel később jelenik meg, mint ez a feladat először.)

3. Kódelemzés

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;
}
  • 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);
}

É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 dereferálása, ha nem lehetett kioptimalizálni: 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;
}

Megoldás után lásd még a következő feladatot.

Fordítási hiba

Alább egy max() függvény, amelyik két érték közül a nagyobbikat választaná ki – beépített típusok esetén érték, objektumok esetén referencia szerint.

#include <type_traits>
#include <iostream>
#include <string>

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

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

int main() {
    int i = 3, j = 4;
    std::cout << max(i, j) << std::endl;
    
    std::string a = "alma", b = "korte";
    std::cout << max(a, b) << std::endl;
}
  1. Jó ötlet ez? Miért nem? :)
  2. Miért nem fordul le a kód?

Lásd még az előző feladatot.

Megoldás

Nem biztos, hogy jó ötlet. Hogy referenciaként vagy másolatként látjuk a nagyobbik értéket, az nem attól kellene függjön, hogy milyen típusú. Lehet, a függvény használója kíváncsi lenne egy tömbelem identitására is, de ha a tömb int-eket tartalmaz, így nem tud választani közülük.

A fordítási hibát az std::max függvény létezése okozza. A max(a, b) hívásnál az a és b változó típusa std névtérbeli std::string, ezért a fordító automatikusan az std névtérbeli függvények között is keres, a Koenig-féle szabálynak megfelelően.

4. Irodalom

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