Links

  • Eugenio Bargiacchi (aka Svalorzen)

  • Programmatore in C++ da 10+ anni

  • Sviluppatore alla Develer

  • Appassionato di AI

  • Email: svalorzen@gmail.com

Chi sono

  • Cos'è la metaprogrammazione

  • Perchè utilizzarla

  • Quali sono le tecniche di base

  • Implementazione di un possibile tool che potrebbe servire in real life

Cosa faremo oggi

Template

C++ è fortemente tipizzato








int x = "string"; // Errore!

Creazione automatica di nuovi tipi, indipendenti l'uno dall'altro

template <typename T> struct A {};

A<int> aInt;
A<double> aDouble;

// ...

template <int N> struct B {};

B<0> bZero;
B<1> bOne;

Template

Riutilizzo dello stesso codice, ma con semantiche diverse

template <typename T>
T add(const T & lhs, const T & rhs) {
    return lhs + rhs;
}

// ...

auto x = add(5, 6);
auto y = add(std::string("a"), std::string("b"));

Template

Controllare il flusso del compilatore per determinare il programma finale.

template <typename T>
struct A {
    void operator() const {
        std::cout << "Classe generica\n";
    }
};

template <>
struct A<int> {
    void operator() const {
        std::cout << "Classe specializzata\n";
    }
};

Meta-Programming

Calcolo di costanti, codice eseguito a compile-time.

Motivazione

Gestione di tipi possibilmente ignoti a seconda delle loro caratteristiche.

Calcolo di costanti, codice eseguito a compile-time.

Motivazione

Gestione di tipi possibilmente ignoti a seconda delle loro caratteristiche.

Partial

Specialization

Meccanismi

SFINAE

(Substitution Failure is

not an Error)

Seleziona una particolare implementazione se i parametri del template corrispondono

template <typename T>
struct A {
    void operator() const {
        std::cout << "Classe generica\n";
    }
};

template <>
struct A<int> {
    void operator() const {
        std::cout << "Classe specializzata\n";
    }
};

// ...

A<double>()(); // Scrive 'Classe generica'
A<int>()();    // Scrive 'Classe specializzata'

Partial Specialization

#include <iostream>

int main() {
    int i = 0;

    if (i == 0)
        std::cout << "\"i\" is equal to zero.\n";
    else
        std::cout << "\"i\" is not equal to zero.\n";

    return 0;
}

Esercitazione

// SINTASSI

template<typename T>        |         template <int N>
struct A {};                |         struct A {};
                            |
template <>                 |         template <>
struct A<int> {};           |         struct A<100> {};
#include <iostream>

int main() {
    char x = 'c', y = 'd';

    if (x == 'a')
        std::cout << "\"x\" is equal to 'a'.\n";
    else if (x == 'b' && y == 'd')
        std::cout << "\"x\" is 'b' and \"y\" is 'd'.\n";
    else
        std::cout << "Values unknown.\n";

    return 0;
}

Esercitazione

// SINTASSI

template<typename T>        |         template <int N>
struct A {};                |         struct A {};
                            |
template <>                 |         template <>
struct A<int> {};           |         struct A<100> {};

Partial Specialization è limitata: può considerare solo tipi conosciuti a priori.

SFINAE

Vogliamo una gestione veramente generica dei tipi a seconda delle loro caratteristiche.

Utilizzo di non-membri o funzioni errate in un template fanno fallire la compilazione

SFINAE

#include <iostream>

struct A { static constexpr int x = 0; };
struct B { static constexpr double y = 1; };
// Sconosciuta a priori
struct C { static constexpr double y = 0; };

template <typename T>
struct Printer {
    void operator()() const { std::cout << T::x << '\n'; }
};

template <>
struct Printer<B> {
    void operator()() const { std::cout << T::y << '\n'; }
};

int main() {
    Printer<C>()(); // Errore di compilazione! x is not a member of C!

    return 0;
};

SFINAE

#include <iostream>

struct A { static constexpr int x = 3; };
struct B { static constexpr int y = 3; };

template <typename T, typename Check = const int>
// template <typename T, typename Check>
struct Printer {
    void operator()() const {
        std::cout << "Questo tipo non ha il membro 'x'.\n";
    }
};

template <typename T>
struct Printer<T, decltype(T::x)> {
    void operator()() const {
        std::cout << "Il membro 'x' di questa classe vale: " << T::x << '\n';
    }
};

// ...

Printer<A>()();
Printer<B>()();

Se una specializzazione parziale dà errore si va avanti

Gli errori "ignorabili" sono solo quelli nella dichiarazione

// Dove mettere gli "errori" nelle classi

template <typename T>
struct ClassName;

template <>
struct ClassName<int>;

// E nelle funzioni

template <typename R, typename T>
R foo(T);

SFINAE

struct A {
    void foo();
};

struct B {

};

// Se il tipo ha un metodo foo() 
// che ritorna void, stampa "FOO!"

// Altrimenti stampa "UNFOO'd!"

Esercitazione

// SINTASSI

template<typename T, typename Check = /* some type */>
struct S;

template<typename T>
struct S<T, decltype(/*something*/)>;
struct A {
    int foo();
};

struct B {
    void foo();
};

struct C {};

// Se il tipo ha un metodo foo() *INDIPENDENTEMENTE
// DAL VALORE DI RITORNO*, allora stampa "ALL FOO!"

// Altrimenti stampa "UNFOO'd!"

Esercitazione

// SINTASSI
template<typename T, typename Check = /* some type */>
struct S;

template<typename T>
struct S<T, decltype(/*something*/)>;

// HINT: l'operatore "," potrebbe aiutarvi!

Verificare stesse interfacce in più punti senza dover copiare.

Incapsulare Traits

Comporre più semplicemente più vincoli contemporanei.

Meccanismo utilizzato nei vari type traits delle STL.

Esporre il risultato di una verifica con un bool o un tipo (per comporre con altri traits)

template <typename T, typename Check = void>
struct hasFoo {
    enum { value = false };
};

template <typename T>
struct hasFoo<T, decltype(std::declval<T>().foo(), void())> {
    using type = decltype(std::declval<T>().foo());
    enum { value = true };
};

// ...

struct A {};
struct B { int foo(); };

std::cout << hasFoo<A>::value << '\n';
std::cout << hasFoo<B>::value << '\n';
std::cout << std::is_same<int, hasFoo<B>::type>::value << '\n';

Incapsulare Traits

Vediamo dei possibili utilizzi produttivi di ciò che abbiamo appena visto.

Intermezzo

Domande?

Best Practices?

Una funzione che chiama un'altra con un sottoinsieme di parametri (quelli giusti)

void foo(char c, int x) {
    std::cout << c << ' ' << x << '\n';
}

int main()
{
    // Prints 'c' 7
    callFunction(foo, 0.2, 'c', "string", 99u, 7); 

    return 0;
}

Obiettivo

Permettono la deduzione o inserimento di più valori o tipi

template <typename... Args>
constexpr int tuple_size(std::tuple<Args...>) {
    return sizeof...(Args);
}

// ...

// Prints 2
std::cout << tuple_size(std::tuple<int, double>()) << '\n';

Template Variadici

Le tuple permettono di gestire facilmente pack di parametri

template <typename... Args>
struct WrapInTuple {
    using type = std::tuple<Args...>;
};

Tuple!

template </* ???? */>
struct getFunctionArguments;

template </* ???? */>
struct getFunctionArguments</* ???? */> {
    using args = /* ???? */ ;
};

// ...

void foo(char, int);
static_assert(std::is_same< 
    std::tuple<char, int>,
    getFunctionArguments<decltype(&foo)>::args
>::value, "No match!");

Esercitazione

// Puntatore a funzione
ReturnType (*)(Arg1, Arg2, Arg3, ...)

// Variadic typename
typename... Args

// Pack expansion
Args...

Utilizzo di istanze di template variadici all'interno del codice

// Parameter pack expansion available in:
// - Function argument  lists (when calling)
// - Function parameter lists (when defining)
// - Template argument  lists (when instantiating/partial specialization)
// - Template parameter lists (when defining)
// - Braced lists initializers
// - Lambda capture lists
// - Some others...

template <typename... Args>
std::tuple<Args...> simple_make_tuple(Args&&... args) {
    //                         <--     pattern      -->  dots
    return std::tuple<Args...>(std::forward<Args>(args)  ...   );
}

Pack Expansion

template </* ???? */>
void print(/* ???? */) {
    // print all arguments separated by '\n';
}

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

template </* ???? */>
void print_tuple_elements(std::tuple</* ???? */>, IdPack</* ???? */> ids) {
    // call print() with the tuple elements specified in ids
}

// ...
// Prints all
print("a", 1, "b", 0.5, 7, 'c', true);
// Prints 1, 0.5, 7
print_tuple_elements(std::make_tuple("a", 1, "b", 0.5, 7, 'c', true),
                     IdPack<1,3,4>());

Esercitazione

// SINTASSI

pattern ...
template <int N>
struct Print {
    void operator()() const {
        std::cout << N << ' ';
        Print<N-1>()();
    }
};

template <>
struct Print<0> {
    void operator()() const {
        std::cout << 0 << '\n';
    }
};

// ...

Print<5>()();
// 5 4 3 2 1 0

Ricorsione

template <typename T>
struct TupleSize;

template <>
struct TupleSize<std::tuple<>> {
    static constexpr auto size = 0;
};

template <typename T, typename... Args>
struct TupleSize<std::tuple<T, Args...>> {
    static constexpr auto size = 1 + 
        TupleSize<std::tuple<Args...>>::size;
};

// ...

// Prints 2
std::cout << TupleSize<std::tuple<int, char>>::size << '\n';

Ricorsione

template </* ???? */>
struct AreTupleTypesEqual;

template </* ???? */>
struct AreTupleTypesEqual</* ???? */> {
    enum { value = /* ???? */ };
};

// ...
// Prints 0
std::cout << AreTupleTypesEqual<std::tuple<int, char>, 
                                std::tuple<char>>::value << '\n';
// Prints 1
std::cout << AreTupleTypesEqual<std::tuple<int, char>, 
                                std::tuple<int, char>>::value << '\n';

Esercitazione

// SINTASSI
template <typename T>
struct TupleUnpack;

template <>
struct TupleUnpack<std::tuple<>>; // Final case

template <typename T, typename... Args>
struct TupleUnpack<std::tuple<T, Args...>>; // Unpacking
template <int M/* , ???? */>
struct TuplesAreEqualUpToImpl;

template <int M/* , ???? */>
struct TuplesAreEqualUpToImpl</* ???? */> {
    static constexpr int N = /* ???? */;
};

template <typename T, typename U>
struct TuplesAreEqualUpTo {
    static constexpr int N = TuplesAreEqualUpToImpl<0, T, U>::N;
};

// ...

static_assert(TuplesAreEqualUpTo<std::tuple<int, double>, 
                                 std::tuple<int, char>>::N == 1, "Error!");

Esercitazione

// SINTASSI
template <typename T>
struct TupleUnpack;

template <>
struct TupleUnpack<std::tuple<>>; // Final case

template <typename T, typename... Args>
struct TupleUnpack<std::tuple<T, Args...>>; // Unpacking

Rivediamo insieme il codice finale del componente più complesso

template <int N, typename T, typename U, int... IDs>
struct Matcher;

template <int N, typename... B, int... IDs>
struct Matcher<N, std::tuple<>, std::tuple<B...>, IDs...> {
    using type = IdPack<IDs...>;
};

template <int N, typename F, typename... A, typename... B, int... IDs>
struct Matcher<N, std::tuple<F, A...>, std::tuple<F, B...>, IDs...> {
    using type = typename Matcher<N+1, std::tuple<A...>, 
                                       std::tuple<B...>, IDs..., N>::type;
};

template <int N, typename FA, typename... A, 
                 typename FB, typename... B, int... IDs>
struct Matcher<N, std::tuple<FA, A...>, std::tuple<FB, B...>, IDs...> {
    using type = typename Matcher<N+1, std::tuple<FA, A...>, 
                                       std::tuple<B...>, IDs...>::type;
};

Qualche aggiunta...

Costruiamo il nostro entry point, e il nostro meta-template è fatto!

template <typename F, typename... Args>
void callFunction(F f, Args&& ...args) {
    using FArgs = typename getFunctionArguments<F>::args;
    using IdList = typename Matcher<0, FArgs, std::tuple<Args...>>::type;

    caller(f, std::make_tuple(args...), IdList());
}

Quasi finito!

Siamo riusciti a realizzare il nostro obiettivo!

void foo(char c, int x) {
    std::cout << c << ' ' << x << '\n';
}

int main()
{
    callFunction(foo, 0.2, 'c', "string", 99u, 7);

    return 0;
}

Mission Accomplished

Grazie della partecipazione!


Domande?

 

Made with Slides.com