13. hét: Paraméterlisták és -továbbítá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. Pont-pont-pont

N dimenziós tömb

Ez itt egy tömb:

Vector<double> v(12);
v[10] = 3.14;

Ez pedig egy mátrix (amelyben a szögletes zárójel operátor helyett a kerek zárójel operátort használjuk indexelésre, mert annak bármennyi paramétere lehet):

Matrix<double> m(3, 4);
m(1, 2) = 3.14;

Általánosítva, egy N dimenziós tömböt így képzelhetnénk el:

Array<double, 5> a(3, 5, 6, 4, 2);  // 3×5×6×4×2-es tömb
a(1, 3, 4, 2, 0) = 3.14;

Az első sablonparaméter a tárolt adatok típusa, a második pedig a dimenziók száma. A konstruktorban adjuk meg a tömb méreteit: pontosan annyi darab egész szám kell legyen, ahány dimenzió van. Az indexeléshez használt függvényhívó operátor szintén pontosan annyi egész számot vár paraméterként, ahány dimenzió van.

Készítsd el az Array osztályt! Ügyelj arra, hogy a fordítási időben kideríthető hiba (helytelen argumentumszám vagy -típus) fordítási hibát eredményezzen!

Megoldás

A megoldás két helyen is C++17 elemet használ, folding expression-t: (... && B) és (1 * ... * sizes). De ezeket nagyon könnyű lenne megírni rekurzívan is.

#include <iostream>
#include <iomanip>
#include <type_traits>
#include <vector>
#include <array>

/* And<true, false, true, ...>::value */
template <bool ... B>
class And : public std::bool_constant<(... && B)> {};

/* AllInt<size_t, int, long>::value */
template <typename... TS>
class AllInt : public And<std::is_convertible<TS, size_t>::value...> {};

template <typename T, size_t DIM>
class Array {
    private:
        /* ez tárolja, hogy melyik irányban mekkora méretű */
        std::array<size_t, DIM> sizes;
        /* ez a tényleges adattömb, leképezve 1 dimenzióra */
        std::vector<T> data;

    public:
        template <typename... ARGS,
                  typename = typename std::enable_if<AllInt<ARGS...>::value>::type,
                  typename = typename std::enable_if<sizeof...(ARGS)==DIM>::type>
        Array(ARGS ... sizes)
            : sizes{size_t(sizes)...}
            , data((1 * ... * sizes))
        {
        }

        template <typename... ARGS,
                  typename = typename std::enable_if<AllInt<ARGS...>::value>::type,
                  typename = typename std::enable_if<sizeof...(ARGS)==DIM>::type>
        T & operator() (ARGS ... indices_)
        {
            std::array<size_t, DIM> indices = { size_t(indices_) ... };
            /* ijesztően néz ki, hogy minden indexelésnél ezt csináljuk, de
             * megijedés helyett érdemes megnézni a lefordított, optimalizált kódot! */
            size_t idx = indices[0];
            for (size_t i = 1; i < DIM; ++i)
                idx = idx*sizes[i-1] + indices[i];
            return data[idx];
        }
};

int main() {
    Array<double, 2> a(3, 4);
    
    for (int i = 0; i < 3; ++i)
        for (int j = 0; j < 4; ++j)
            a(i, j) = (i+1)*(j+1);
            
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j)
            std::cout << std::setw(10) << a(i, j);
        std::cout << std::endl;
    }
}

2. Paramétertovábbítás

Saját std::ref

Írd meg az std::ref-et! Emlékeztetőül: ez arra jó, hogy létrehozzon egy funktort, amely egy másik funktorra hivatkozik, de referencia szerint. Így a létrehozott funktor akárhányszor másolható, mindig az eredeti fog dolgozni. Ahogy az std::ref is működik:

class IntPrinter {
  private:
    int count = 0;
  public:
    void operator() (int i) {
        std::cout << ++count << ". " << i << std::endl;
    }
};
 
int arr1[] = { 5, 9, 2, 4 };
int arr2[] = { 5, 8, 4, 7, 9 };
IntPrinter p;
std::for_each(std::begin(arr1), std::end(arr1), std::ref(p));     /* 1. 2. 3. 4. */
std::for_each(std::begin(arr2), std::end(arr2), std::ref(p));     /* 5. 6. 7. 8. 9. */

Figyelj arra, hogy a becsomagolt függvényobjektumnak tetszőlegesen sok(féle) függvényhívó operátora lehet, és ezeknek típushelyesen kell továbbadni a paramétereket! (Az std::ref() egyébként nem osztály, hanem egy gyártófüggvény.)

Megoldás
/* Ez itt a referencia objektum */
template <typename FUNCTOR>
class RefProxy {
  private:
    /* Amely a függvényobjektumot referencia szerint tartalmazza */
    FUNCTOR &f;
  public:
    /* A konstruktora eltárolja a referenciát */
    RefProxy(FUNCTOR & f) : f(f) {}

    /* És ha bármilyen paraméterekkel meghívják */
    template <typename... ARGS>
    auto operator() (ARGS && ... args)
      /* Akkor egy ugyanolyan típusú értékkel tér vissza, mint
       * amilyennel a függvényobjektum visszatérne */
      -> decltype(f(std::forward<ARGS...>(args...)))
    {
        /* És továbbpasszolja a paramétereket
         * a hivatkozott függvényobjektumnak */
        return f(std::forward<ARGS...>(args...));
    }
};


/* Ez a függvény hozza létre a RefProxy-t. Tulajdonképpen csak
 * azért van, hogy kitalálja a FUNCTOR típusát. */
template <typename FUNCTOR>
auto make_refproxy(FUNCTOR & f) -> RefProxy<FUNCTOR> {
    return RefProxy<FUNCTOR>{f};
}

Saját std::function

Írd meg az std::function-t! Ha mindent jól csinálsz, akkor az alábbi kódban az std::function helyére my_function-t írhatsz majd:

#include <functional>
#include <iostream>
#include <cmath>

int main() {
    std::function<double(double)> f;
    
    if (!f)
        std::cout << "Egyelőre nullptr" << std::endl;
    
    f = sin;
    std::cout << sin(2.3) << "==" << f(2.3) << std::endl;
    
    f = [] (double x) { return x*x; };
    std::cout << 2.3*2.3 << "==" << f(2.3) << std::endl;
    
    f = std::bind(pow, std::placeholders::_1, 4);
    std::cout << pow(2.3, 4) << "==" << f(2.3) << std::endl;

    auto f2 = f;    /* másolható */
    std::cout << pow(2.3, 4) << "==" << f2(2.3) << std::endl;
    
    f = nullptr;
    try {
        f(2.3);
    } catch (std::bad_function_call &e) {
        std::cout << "Megint nullptr" << std::endl;
    }
}
Tipp

Ehhez az osztályodnak egy olyan operator= kell, amelyik tetszőleges objektumot (függvénypointert, lambdát, ...) át tud venni, és le tud másolni. Ezért a lemásolt objektumot be kell majd csomagolnod egy osztályba, aminek típusa a lemásolt objektumtól függ. Hogy erre lehessen hivatkozni, a különféle elképzelhető csomagoló osztályokhoz szükség van egy közös ősosztályra.

Figyelj arra, hogyan adod át a becsomagolt függvénynek a paramétereket! Meg arra is, hogy egy std::function lemásolásakor lemásolódik a bele csomagolt funktort is. Ez azért fontos, mert a funktornak belső állapota lehet. Az std::function ilyen jellegű viselkedését is tudnia kell az osztálynak.

Saját std::function – még egyszer

Előbb oldd meg a fenti feladatot.

Ha megvan, akkor nézd át a kódod, mennyit foglalkoztál memóriakezeléssel. Ha sokat, akkor egy okos pointerre van szükséged. (Melyikre?) Használj ilyet, ügyelve arra, hogy a lehető legkevesebb erőforráskezelő függvényt (másoló konstruktor, destruktor stb.) kelljen megírnod!

Megoldás

Egy std::unique_ptr kell. A function objektum másoló konstruktorát ilyenkor is meg kell írni (mert klónozni kell benne), viszont a mozgató konstruktor és a destruktor lehet default. Ugyanez a helyzet az értékadó operátorokkal, amelyek megírhatók egyben, move and swap segítségével.

Mit csinál a függvény?

Mit csinál az alábbi függvény? Az általa visszaadott függvény, amely hívásmódjában kompatibilis az első paramétereként megadott függvénnyel (ugyanolyan paraméterek és visszatérési érték), miben viselkedik másképpen? Írj kódot a működés bemutatására!

template <typename RET, typename... ARGS>
auto memoized_func(RET (*func)(ARGS...)) -> std::function<RET(ARGS...)> {
    std::map<std::tuple<ARGS...>, RET> retvals;

    auto memoized = [=] (ARGS... args) mutable -> RET {
        auto args_tuple = std::make_tuple(args...);
        auto found = retvals.find(args_tuple);
        if (found != retvals.end())
            return found->second;

        auto retval = func(args...);
        retvals.insert(make_pair(args_tuple, retval));
        return retval;
    };

    return memoized;
}

Extra kérdések: Mikor érdemes ilyet használni? Mikor nem szabad ilyet használni?

Megoldás

Becsomagolja a paraméterként adott függvényt egy másik függvénybe, amely ellenőrzi azt, hogy hívták-e már pontosan azokkal a paraméterekkel, mint amiket most lát. Ha igen, akkor nem értékeli ki újra a függvényt, hanem csak visszatér az eltárolt értékkel. Ha nem, akkor meghívja és eltárolja a visszaadott értékét, hogy legközelebb már ne kelljen.

double sin_with_print(double x) {
    std::cout << "sin_with_print(x) called\n";
    return sin(x);
}

int main() {
    auto msin = memoized_func(sin_with_print);
    std::cout << msin(1.0) << std::endl;
    std::cout << msin(1.0) << std::endl;
    std::cout << msin(2.0) << std::endl;
    std::cout << msin(2.0) << std::endl;
    std::cout << msin(1.0) << std::endl;
    std::cout << msin(1.0) << std::endl;
}
sin_with_print(x) called
0.841471                sin(1.0) először
0.841471
sin_with_print(x) called
0.909297                sin(2.0) először
0.909297
0.841471
0.841471

Akkor érdemes használni, ha a függvény kiértékelése lassú – sokkal lassabb, mint amennyi ideig a map-ben keresés tart. Nem szabad használni, ha a meghívott függvénynek mellékhatása van, mert a mellékhatás csak egyszer fog megtörténni (adott paraméterek mellett mindig az első híváskor).

PHP list() I. – a könnyebbik fele

A PHP-ban egy sztringben adott, valamilyen szeparátor karakterrel elválasztott adatokat egyetlen egy sorral „ki lehet csomagolni” egy rakat változóba. Például:

list($a, $b) = explode(" ", "3 4.5");   // $a = "3"; $b = "4.5";

Tekintsünk most el a szeparátor karakter megadásától, vegyük azt fixen szóköznek. A feladat egy olyan C++ függvényt írni, amely a fenti nyelvi szerkezethez hasonlóan tetszőlegesen sok, tetszőleges típusú változó értékét ki tudja venni egy sztringből, ráadásul típushelyesen. Például:

int a;
double b;
unpack("3 4.5", a, b);  // a = 3; b = 4.5;

Útmutatás: a sztring darabolásához is használhatsz istringstream-et. Használd az egyes típusok >> operátorát! A felbontandó sztring futási időben is keletkezhet, így biztosan nem fordítási időben kell darabolni.

A feladatnak van egy morcosabb második része is.

Megoldás

Klasszikus megoldás:

#include <iostream>
#include <string>
#include <sstream>

void set_vars(std::istringstream &) {
}

template <typename HEAD, typename... TAIL>
void set_vars(std::istringstream & is, HEAD & arg, TAIL &... tail) {
    is >> arg;
    set_vars(is, tail...);
}

template <typename... ARGS>
void unpack(std::string const & words, ARGS & ... args) {
    std::istringstream is(words);
    set_vars(is, args...);
}


int main() {
    int a;
    double b;

    unpack("3 4.5", a, b);
    std::cout << a << " " << b;
}

Esetleg C++17-ben:

C++17
template <typename ... ARGS>
void unpack(std::string const & str, ARGS & ... args) {
    std::istringstream is(str);
    (is >> ... >> args);
}

Vagy C++11-ben, a swallow{}-os trükkel:

template <typename ... ARGS>
void unpack(std::string const & str, ARGS & ... args) {
    std::istringstream is(str);
    
    using swallow = int[];
    swallow{0, (void(is >> args), 0)...};
}

PHP list() II. – a nehezebbik fele

A feladat hasonló, mint az első részben, de most olyan kódot kell írnod, amelynek a szintaktikája már teljesen megegyezik a PHP-s list() nyelvi szerkezetével:

int a;
double b;
list(a, b) = explode("3 4.5");

Útmutatás: indulj ki a list() függvényből! Ennek létre kell hoznia egy referenciákat tartalmazó segédobjektumot, amely a feldarabolt sztringet át tudja venni az értékadó operátorával. Felhasználhatod az std::tuple osztályt is. Vigyázat, ez jóval nehezebb feladat, mint az első fele!

Megoldás
C++17
#include <sstream>
#include <string>
#include <iostream>
#include <tuple>

template <typename ... TYPES>
class List {
    private:
        std::tuple<TYPES & ...> refs;
        
        template <int N>
        void unpack(std::istringstream & is) {
            if constexpr (N != sizeof...(TYPES)) {
                is >> std::get<N>(refs);
                unpack<N+1>(is);
            }
        }
    
    
    public:
        List(TYPES & ... args) : refs(args...) {}
        
        void operator=(std::string s) {
            std::istringstream is(s);
            unpack<0>(is);
        }
};

template <typename ... TYPES>
auto list(TYPES & ... args) {
    return List<TYPES & ...>(args...);
}

std::string explode(std::string s) {
    return s;
}

int main() {
    int a;
    double b;
    list(a, b) = explode("3 4.5");
}

Python x,y = y,x

Pythonban az alábbi sor megcseréli két változó tartalmát:

x,y = y,x

Ez a trükk C++11-ben is használható. Ha nem is a vessző operátorral, mert az C++-ban teljesen mást jelent, az std::tie() és std::make_tuple() függvények segítségével megoldható:

std::tie(x, y) = std::make_tuple(y, x);

A feladatod: írj tetszőleges kódrészletet, amely szabványos std::pair osztályt használva megoldja a feladatot:

my_func_1(x, y) = my_func_2(y, x);

A trükk az, hogy a függvények becsomagolják a változókat valamilyen segédobjektum(ok)ba, amely(ek) értékadó operátora fogja elvégezni az értékek másolását. Ha jó a megoldásod, akkor ez nem csak cserére használható, hanem bármilyen adatpárok értékadására, pl. a,b = c,d mintájára.

3. Rekurzív sablonok

A legnagyobb

Adott az alábbi maximumkeresés:

#include <iostream>

int greatest(int arr[], int size) {
    int max = arr[0];
    for (int i = 1; i != size; ++i)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

int main() {
    int a[] = { 4, 87, 2, 65, 89, 1 };
    std::cout << greatest(a, 6) << std::endl;
}

Írd át ezt jobbrekurzív függvénnyé! Az if() elágazást érdemes előbb egy ?: operátoros kifejezéssel helyettesíteni.

Megoldás
int greatest(int arr[], int size, int max) {
    if (size == 0)
        return max;
    else
        return greatest(arr+1, size-1, arr[0] > max ? arr[0] : max);
}
int greatest(int arr[], int size) {
    return greatest(arr+1, size-1, arr[0]);
}

Utána pedig írd át sablon metaprogrammá, amelyben a Greater nevű sablon akárhány számot kaphat sablonparaméterként! A különálló maximumkereső, és a rekurziót indító két függvényből két külön osztály lesz.

Megoldás
#include <iostream>

template <int max, int... numbers>
struct GreatestHelper;

template <int max, int head, int... tail>  // 1
struct GreatestHelper<max, head, tail...> {
    static constexpr int value = GreatestHelper<(head > max) ? head : max, tail...>::value;
};

template <int max>
struct GreatestHelper<max> {               // 2
    static constexpr int value = max;
};

template <int head, int... tail>
struct Greatest {
    static constexpr int value = GreatestHelper<head, tail...>::value;
};

int main() {
    std::cout << Greatest<4, 87, 2, 65, 89, 1>::value << std::endl;
}

A fenti kódban a Greatest osztály indítja el a rekurziót, a GreatestHelper osztály példányosításával. A GreatestHelper osztály első paramétere, a max nevű, a rekurzióban használt gyűjtőparaméter; a megvizsgált számok közül ez tárolja a legnagyobbat. (A rekurzió indulásakor ez a sorozat első elemét kapja.)

A segédosztálynak két specializációja van. Az 1-gyessel jelöltet használja a fordító akkor, ha legalább két sablonparaméter van (a gyűjtőparaméter és a vizsgálandó számok), a 2-es változatot pedig akkor, ha már nincs vizsgálandó szám. Az eredeti osztálydeklaráció célja csak annyi, hogy megadja a fordító számára a sablonparamétereket, utána már lehet specializálni az osztályt.

Természetesen működne az a módszer is, ahol az általános eset a legalább két paraméterű, az egyetlen specializáció pedig a pontosan két paraméterű. De így tartalmaz a megoldás egy kis kódduplikációt, tehát a fenti megoldás szebb.

template <int max, int head, int... tail>
struct GreatestHelper {
    static constexpr int value = GreatestHelper<(head > max) ? head : max, tail...>::value;
};
template <int max, int head>
struct GreatestHelper<max, head> {
    static constexpr int value = head > max ? head : max;
};

Mit csinál a sablon?

Mit csinál ez a két osztálysablon? Próbáld ki, hogy példányosítod valamilyen kicsi egész számmal (papíron vagy jegyzettömbben)! Pl. mivel egyenértékű a gens<4>::type belső típus?

template<int ...>
struct seq { };

template<int N, int ...S>
struct gens : gens<N-1, N-1, S...> { };

template<int ...S>
struct gens<0, S...> {
    using type = seq<S...>;
};
Megoldás
/* gens<4> -> N = 4, S = üres */
class gens<4> : gens<3, 3> { };

/* gens<3, 3> -> N = 3, S = 3 */
class gens<3, 3> : gens<2, 2, 3> { };

/* gens<2, 2, 3> -> N = 2, S = 2, 3 */
class gens<2, 2, 3> : gens<1, 1, 2, 3> { };

/* gens<1, 1, 2, 3> -> N = 1, S = 1, 2, 3 */
class gens<1, 1, 2, 3> : gens<0, 0, 1, 2, 3> { };

/* gens<0, 0, 1, 2, 3> -> specializáció! nincs N, S = 0, 1, 2, 3 */
class gens<0, 1, 2, 3> {
    using type = seq<0, 1, 2, 3>;
};

És ezt a type belső típust megörökik az előző, meg az azelőtti stb. osztályok, tehát végül gens<4>::type = seq<0, 1, 2, 3>. A gens<N> osztály előállítja seq osztály sablonparamétereként a 0 ... N-1 természetes számokat.

A template metaprogramozás sűrűje

Az std::tuple osztálysablonnak bármennyi sablonparamétere lehet: az ott megadott típusú adatokat tudja eltárolni. Pl. std::tuple<int, double> egy egész és egy valós számot tárol. Ha adott egy ilyen objektum t néven, std::get<0>(t) veszi ki belőle az egész számot, std::get<1>(t) pedig a valósat.

Tegyük fel, hogy van egy függvényük, amely épp egy int és egy double paramétert vár. És azt, hogy van egy tuple-ünk, amelyben egy egész és egy valós szám van. A feladat: meghívni a függvényt a tuple-ben tárolt adatokkal. Egészen pontosan, a feladat: írni egy olyan osztályt, amely képes eltárolni egy tetszőleges típusú (paraméterezésű) függvényre mutató pointert, továbbá a függvény paraméterezésének megfelelő objektumokat tartalmazó tuple-t – hogy aztán valamikor azt a függvényt, azokkal a paraméterekkel, meg lehessen hívni. Így tetszőlegesen eltárolható egy „felparaméterezett” függvény, hogy később, egy tetszőelges időpontban meg lehessen hívni.

Az alábbi kód ezt a problémát oldja meg. Elemezd a kódot, értsd meg a működését!

#include <tuple>
#include <iostream>


template<int ...> struct seq {};
template<int N, int ...S> struct gens : gens<N-1, N-1, S...> {};
template<int ...S> struct gens<0, S...>{ using type = seq<S...>; };


double foo(int x, float y, double z) {
    return x + y + z;
}


template <typename Ret, typename ...Args>
struct save_it_for_later {
    Ret (*func)(Args...);
    std::tuple<Args...> params;

    template<int ...S>
    Ret callFunc(seq<S...>) {
        return func(std::get<S>(params) ...);
    }

    Ret delayed_dispatch() {
        return callFunc(typename gens<sizeof...(Args)>::type());
    }
};


int main(void) {
    std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
    save_it_for_later<double, int, float, double> saved = {foo, t};

    std::cout << saved.delayed_dispatch() << std::endl;
}

A C++14 tartalmaz ehhez hasonló eszközöket is.

Rekurzió

Sablon metaprogramokban listákat is kellhet tárolnunk: általában erre egy segédosztály sablonparamétereit szoktuk használni. Például:

template <int...>
class Szamok {};

using Hatvanyok = Szamok<1, 2, 4, 8, 16>;

A feladat: írj ehhez hasonló osztályt, amely a sablonparamétereiben adott számok összegét, illetve a számsor hosszát is megadja statikus tagváltozóként! Például:

int main() {
    std::cout << SumLen<1,2,3>::value;      /* 1+2+3 = 6 */
    std::cout << SumLen<1,2,3>::length;     /* 3 db szám */
}
Megoldás
template <int... S>
class SumLen;

template <int X, int... XS>
class SumLen<X, XS...> {
  private:
      using Rest = SumLen<XS...>;

  public:
    static constexpr int value  = X + Rest::value;
    static constexpr int length = 1 + Rest::length;
};

template <>
class SumLen<> {
  public:
    static constexpr int value  = 0;
    static constexpr int length = 0;
};

Paraméterként

Írd meg úgy az előző programot, hogy a SumLen osztály csak a műveleteket végezze, a Szamok osztály pedig csak a tárolást! Ebben az esetben a SumLen paraméterként kell kapja a Szamok osztályt:

int main() {
    std::cout << SumLen<Szamok<1,2,3>>::value;
    std::cout << SumLen<Szamok<1,2,3>>::length;
}
Megoldás

A megoldásban a SumLen osztálynak egy sablonparamétere van csak, egy típus. Egy Szamok>X, XS...> paraméterű részleges specializáció lehet az, amelyik az egyetlen egy sablonparaméterből, a típusból „ki tudja csomagolni” ezeket az egész számokat.

template <int... S>
class Szamok;

template <typename SZAMOK>
class SumLen;

template <int X, int... XS>
class SumLen<Szamok<X, XS...>> {
  private:
    using Rest = SumLen<Szamok<XS...>>;

  public:
    static constexpr int value  = X + Rest::value;
    static constexpr int length = 1 + Rest::length;
};

template <>
class SumLen<Szamok<>> {
  public:
    static constexpr int value  = 0;
    static constexpr int length = 0;
};

N-edik típus

Írj sablon osztályt, amelyik átvesz egy egész számot és egy típuslistát sablonparaméterként! Az osztály feladata, hogy a listából kiválassza valamelyik típust; az annyiadik sorszámút, amennyi a megadott egész szám.

Például: NthType<0, int, double char>::Type-nál ez int. NthType<2, int, double, char>-nál pedig char.

Megoldás

Lásd a következő két feladatnál.

Típusok listája

Definiálj osztályt, amelynek sablonparamétereit típusnevek tárolására lehet használni!

using MyTypes = TypeList<int, double, char>;

Definiálj metafüggvényt, amellyel a típuslistából lekérdezhető valamelyik típus! (Ez nem teljesen ugyanaz, mint az előző feladatnál; itt nem a típusnevek adják a függvény paramétereit, hanem a típusneveket egy osztály sablonparaméterében tárolod.)

using SecondType = typename NthType<MyTypes, 2>::type;
static_assert(std::is_same<char, SecondType>);

Írj függvényt, amellyel lekérdezhető, hogy a listában szerepel-e egy adott típus!

Megoldás

C++17-ben ez nagyon könnyű folding expression segítségével. A típusokat tartalmazó osztály nélkül:

template <typename T, typename ... LIST>
struct IsTypeInList {
    static constexpr bool value = (std::is_same<T, LIST>::value || ...);
};

Klasszikus megoldás rekurzióval:

template <typename T, typename ... LIST>
struct IsTypeInList;

template <typename T>
struct IsTypeInList<T> {    /* üres listára */
    static constexpr bool value = false;
};

template <typename T, typename HEAD, typename ... TAIL>
struct IsTypeInList<T, HEAD, TAIL...> { /* egy elemű vagy annál hosszabb listára */
    /* az első az, vagy a többi közt kell legyen */
    static constexpr bool value =
        std::is_same<T, HEAD>::value || IsTypeInList<T, TAIL...>::value;
};

Definiálj metafüggvényt, amelyik megadja, hogy a típuslistában csak különféle típusok szerepelnek-e!

constexpr bool b1 = AllTypesDifferent<TypeList<int, double, char>>::value;
static_assert(b1 == true);

constexpr bool b2 = AllTypesDifferent<TypeList<int, double, int>>::value;
static_assert(b2 == false);

Milyen értéket ad a függvény?

Amikor az std::bind segítségével létrehozunk egy új függvényt, akkor annak visszatérési értéke megegyezik az eredeti függvényével. Például std::bind(sqrt, 2) egy nulla paraméterű függvényt hoz létre, amely mindig az sqrt(2) értékét adja: double értékű függvény jött létre, mert a becsomagolt double sqrt(double) függvény is ilyet adott.

Írj sablon osztályt, aminek paramétere egy függvény fejléce (call signature), és megadja, hogy milyen visszatérési értékkel rendelkezik ez a függvény! Például RetType<double(int)>::type esetén adjon double-t.

Specializáld az osztályt, hogy működjön függvénypointerrel és std::function-nel is!

Megoldás

Lásd a következő két feladatnál.

Mi a függvény paramétere?

Adott az alábbi függvény:

void f(int i, double d);

Ha ebből az std::bind segítségével kreálunk egy másikat, az f2 nevűt:

using namespace std::placeholders;
auto f2 = std::bind(f, _2, _1);    // f2(a, b) -> f(b, a)

Akkor f2 egy f2(double, int) paraméterezéssel hívható függvény lett, mert az első paraméteréből az f második paramétere lesz (ami double), és második paraméteréből az f első paramétere (ami int).

Írj segédosztályt, amelyik egy függvényről képes megmondani, hogy milyen típuső annak a valahányadik paramétere! Pl. NthArg<0, void(int, double)>::type legyen int, mert a paraméterlistán int az első adat.

Specializáld az osztályt, hogy működjön függvénypointerre és std::function típusra is!

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

/* NthType - N-edik típus kiválasztása egy listából */
namespace {
    template <unsigned N, typename... TYPES>
    class NthTypeImpl;

    template <typename HEAD, typename ... TAIL>
    class NthTypeImpl<0, HEAD, TAIL...> {
        public:
            using type = HEAD;
    };

    template <unsigned N, typename HEAD, typename ... TAIL>
    class NthTypeImpl<N, HEAD, TAIL...> : public NthTypeImpl<N - 1, TAIL...> {};
}

template <unsigned N, typename... TYPES>
using NthType = typename NthTypeImpl<N, TYPES...>::type;

/* RetVal - Mi a függvény visszatérési értéke? */
namespace {
    template <typename T>
    class RetValImpl;

    template <typename RET, typename ... ARGS>
    class RetValImpl<RET(ARGS...)> {
        public:
            using type = RET;
    };

    template <typename CALLSIG>
    class RetValImpl<CALLSIG*> : public RetValImpl<CALLSIG> {};

    template <typename CALLSIG>
    class RetValImpl<std::function<CALLSIG>> : public RetValImpl<CALLSIG> {};
}

template <typename T>
using RetVal = typename RetValImpl<T>::type;

/* NthArg - Mi a függvény N-edik paramétere? */
namespace {
    template <unsigned N, typename T>
    class NthArgImpl;
    
    template <unsigned N, typename RET, typename ... ARGS>
    class NthArgImpl<N, RET(ARGS...)> {
        public:
            using type = NthType<N, ARGS...>;
    };

    template <unsigned N, typename CALLSIG>
    class NthArgImpl<N, CALLSIG*> : public NthArgImpl<N, CALLSIG> {};

    template <unsigned N, typename CALLSIG>
    class NthArgImpl<N, std::function<CALLSIG>> : public NthArgImpl<N, CALLSIG> {};
}

template <unsigned N, typename T>
using NthArg = typename NthArgImpl<N, T>::type;


int main() {
    /* is_same_v = C++17. Ha nem akar fordulni, akkor is_same<X, Y>::value. */
    static_assert(std::is_same_v<char, NthType<2, int, double, char>>);
    static_assert(std::is_same_v<int, RetVal<int()>>);
    static_assert(std::is_same_v<int, RetVal<int(*)()>>);
    static_assert(std::is_same_v<int, RetVal<std::function<int()>>>);
    static_assert(std::is_same_v<double, NthArg<2, void(int, char, double)>>);
}

Saját tuple I.

Írj my_tuple osztályt, mint az std::tuple! Ennek tetszőlegesen sok sablonparamétere lehet, és olyan típusú adatokat tárol, mint amilyeneket itt kapott. Pl. my_tuple>int, char> egy egész számot és egy karaktert tartalmaz. Az egyes adatelemeket lehessen a my_get<n>(t) függvénnyel elérni!

my_tuple<int, char> t(5, 'X');

std::cout << my_get<0>(t);       // 5
my_get<1>(t) = 'Y';

A feladat megoldásához rekurzívan kell példányosítania a my_tuple osztálynak adattagként saját magát: mindig az egyik adattagot tartalmazva sajátjaként, és a többit rekurzívan.

Ellenőrizd, hogy az előbb megírt osztályod referenciákat is tud-e tárolni!

int a, int b;
my_tuple<int &, int &> t(a, b);

assert(&a == &my_get<0>(t));
assert(&b == &my_get<1>(t));

Saját tuple II.

Írj my_make_tuple függvényt, amelyik érték szerint eltárolja a neki paraméterként adott adatokat egy my_tuple objektumban! Írj my_tie függvényt, amelyik referencia szerint tárolja el azokat!

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

/* Saját tuple I. */
template <typename ...>
class my_tuple;

/* üres tuple, megállítja a rekurziót */
template <>
class my_tuple<> {};

template <typename HEAD, typename ... TAIL>
class my_tuple<HEAD, TAIL...> {
  private:
    /* az első paraméternek megfelelő adat */
    HEAD data;
    /* a többi adat egy egyszerűbb tuple-ben */
    my_tuple<TAIL...> more_data;
  public:
    my_tuple() {}
    template <typename HEADP, typename ... TAILP>
    my_tuple(HEADP && head, TAILP && ... tail)
        : data(std::forward<HEADP>(head))
        , more_data(std::forward<TAILP>(tail)...) {}
    
    /* erre a my_tuple_get-nek lesz szüksége.
     * lehetne valami publikus getter is, de a rekurzív
     * példányosítás implementációs kérdés. */
    template <size_t, typename ...>
    friend class my_tuple_get;
};

/* segédosztály a my_tuple_get függvényhez,
 * mert részleges specializációra van szükség */
template <size_t IDX, typename ... TAIL>
class my_tuple_get;

template <size_t IDX, typename HEAD, typename ... TAIL>
struct my_tuple_get<IDX, HEAD, TAIL...> {
    static auto & my_get(my_tuple<HEAD, TAIL...> & t) {
        return my_tuple_get<IDX-1, TAIL...>::my_get(t.more_data);
    }
};

template <typename HEAD, typename ... TAIL>
struct my_tuple_get<0, HEAD, TAIL...> {
  public:
    static auto & my_get(my_tuple<HEAD, TAIL...> & t) {
        return t.data;
    }
};

/* a megírandó my_get függvény delegál a my_tuple_get
 * osztály statikus tagfüggvényének */
template <size_t IDX, typename ... T>
auto & my_get(my_tuple<T...> & t) {
    return my_tuple_get<IDX, T...>::my_get(t);
}

/* Saját tuple II. */
template <typename ... T>
auto my_make_tuple(T ... data) {
    return my_tuple<T...>(std::move(data)...);
}

template <typename ... T>
auto my_tie(T &... data) {
    return my_tuple<T &...>(data...);
}


int main() {
    my_tuple<int, double> t1;
    my_tuple<int, double> t2(5, 1.2);
    auto t3 = my_make_tuple(5, 1.2);
    
    my_get<0>(t1) = 2;
    std::cout << my_get<1>(t2) << std::endl;
    std::cout << my_get<1>(t3) << std::endl;
    
    int a = 2, b = 3;
    auto t4 = my_tie(a, b);
    my_get<1>(t4) = 5;
    std::cout << b;
}