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?
Introduzione al C++ Template Metaprogramming
By svalorzen
Introduzione al C++ Template Metaprogramming
- 1,992