C++
metaprogrammazione
Ricerca…
introduzione
In C ++ Metaprogramming si riferisce all'uso di macro o modelli per generare codice in fase di compilazione.
In generale, le macro sono disapprovate in questo ruolo e i modelli sono preferiti, sebbene non siano così generici.
La metaprogrammazione dei modelli fa spesso uso di calcoli in fase di compilazione, tramite modelli o funzioni di constexpr
, per raggiungere i propri obiettivi di generazione del codice, tuttavia i calcoli in fase di compilazione non sono di per sé metaprogrammazione.
Osservazioni
Metaprogrammazione (o più specificamente, Metaprogrammazione di modelli) è la pratica dell'uso di modelli per creare costanti, funzioni o strutture di dati in fase di compilazione. Ciò consente di eseguire calcoli una volta al momento della compilazione anziché in ogni periodo di esecuzione.
Calcolo dei fattori
I fattoriali possono essere calcolati in fase di compilazione utilizzando tecniche di metaprogrammazione del modello.
#include <iostream>
template<unsigned int n>
struct factorial
{
enum
{
value = n * factorial<n - 1>::value
};
};
template<>
struct factorial<0>
{
enum { value = 1 };
};
int main()
{
std::cout << factorial<7>::value << std::endl; // prints "5040"
}
factorial
è una struttura, ma nella metaprogrammazione del modello viene considerata una metafunzione del modello. Per convenzione, i metafunzionamenti dei template vengono valutati controllando un particolare membro, ::type
per i metafunzioni che danno come risultato tipi: ::value
per metafuntion che generano valori.
Nel codice sopra, valutiamo la metafunzione factorial
istanziando il modello con i parametri che vogliamo passare, e usando il ::value
per ottenere il risultato della valutazione.
La metafunzione stessa si basa sull'istanza ricorsiva della stessa metafunzione con valori più piccoli. La specializzazione factorial<0>
rappresenta la condizione di chiusura. La metaprogrammazione dei modelli ha la maggior parte delle restrizioni di un linguaggio di programmazione funzionale , quindi la ricorsione è il costrutto di "looping" primario.
Poiché i metafuncoli del modello vengono eseguiti in fase di compilazione, i risultati possono essere utilizzati in contesti che richiedono valori in fase di compilazione. Per esempio:
int my_array[factorial<5>::value];
Gli array automatici devono avere una dimensione definita in fase di compilazione. E il risultato di una metafunzione è una costante in fase di compilazione, quindi può essere utilizzata qui.
Limitazione : la maggior parte dei compilatori non consente la profondità di ricorsione oltre un limite. Ad esempio, il compilatore g++
per impostazione predefinita limita la ricorsione a 256 livelli. Nel caso di g++
, il programmatore può impostare la profondità della ricorsione usando l' -ftemplate-depth-X
.
Dal momento che C ++ 11, il modello std::integral_constant
può essere utilizzato per questo tipo di calcolo del modello:
#include <iostream>
#include <type_traits>
template<long long n>
struct factorial :
std::integral_constant<long long, n * factorial<n - 1>::value> {};
template<>
struct factorial<0> :
std::integral_constant<long long, 1> {};
int main()
{
std::cout << factorial<7>::value << std::endl; // prints "5040"
}
Inoltre, constexpr
funzioni di constexpr
diventano un'alternativa più pulita.
#include <iostream>
constexpr long long factorial(long long n)
{
return (n == 0) ? 1 : n * factorial(n - 1);
}
int main()
{
char test[factorial(3)];
std::cout << factorial(7) << '\n';
}
Il corpo di factorial()
è scritto come una singola istruzione perché in C ++ 11 constexpr
funzioni di constexpr
possono usare solo un sottoinsieme piuttosto limitato della lingua.
Dal momento che C ++ 14, molte restrizioni per constexpr
funzioni di constexpr
sono state eliminate e ora possono essere scritte in modo molto più pratico:
constexpr long long factorial(long long n)
{
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
O anche:
constexpr long long factorial(int n)
{
long long result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
Dal momento che c ++ 17 si può usare espressione di piega per calcolare fattoriale:
#include <iostream>
#include <utility>
template <class T, T N, class I = std::make_integer_sequence<T, N>>
struct factorial;
template <class T, T N, T... Is>
struct factorial<T,N,std::index_sequence<T, Is...>> {
static constexpr T value = (static_cast<T>(1) * ... * (Is + 1));
};
int main() {
std::cout << factorial<int, 5>::value << std::endl;
}
Iterazione su un pacchetto di parametri
Spesso, è necessario eseguire un'operazione su ogni elemento di un pacchetto di parametri modello variadic. Ci sono molti modi per farlo e le soluzioni diventano più facili da leggere e scrivere con C ++ 17. Supponiamo di voler semplicemente stampare ogni elemento in un pacchetto. La soluzione più semplice è recitare:
void print_all(std::ostream& os) {
// base case
}
template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
os << first;
print_all(os, rest...);
}
Potremmo invece utilizzare il trucco di espansione, per eseguire tutto lo streaming in una singola funzione. Questo ha il vantaggio di non aver bisogno di un secondo sovraccarico, ma ha lo svantaggio di una lettura inferiore a quella stellare:
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
using expander = int[];
(void)expander{0,
(void(os << args), 0)...
};
}
Per una spiegazione di come funziona, vedi l'eccellente risposta di TC .
Con C ++ 17, otteniamo due potenti nuovi strumenti nel nostro arsenale per risolvere questo problema. Il primo è un'espressione di piegatura:
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
((os << args), ...);
}
E il secondo è if constexpr
, che ci consente di scrivere la nostra soluzione ricorsiva originale in una singola funzione:
template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
os << first;
if constexpr (sizeof...(rest) > 0) {
// this line will only be instantiated if there are further
// arguments. if rest... is empty, there will be no call to
// print_all(os).
print_all(os, rest...);
}
}
Iterazione con std :: integer_sequence
Dal momento che C ++ 14, lo standard fornisce il modello di classe
template <class T, T... Ints>
class integer_sequence;
template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;
e una metafunzione generatrice per questo:
template <class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;
Anche se questo è standard in C ++ 14, questo può essere implementato usando gli strumenti C ++ 11.
Possiamo usare questo strumento per chiamare una funzione con una std::tuple
di argomenti (standardizzata in C ++ 17 come std::apply
):
namespace detail {
template <class F, class Tuple, std::size_t... Is>
decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) {
return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...);
}
}
template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tpl) {
return detail::apply_impl(std::forward<F>(f),
std::forward<Tuple>(tpl),
std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}
// this will print 3
int f(int, char, double);
auto some_args = std::make_tuple(42, 'x', 3.14);
int r = apply(f, some_args); // calls f(42, 'x', 3.14)
Invio di tag
Un modo semplice per selezionare le funzioni al momento della compilazione è di inviare una funzione a una coppia di funzioni sovraccariche che accettano un tag come un argomento (di solito l'ultimo). Ad esempio, per implementare std::advance()
, possiamo effettuare la spedizione sulla categoria iteratore:
namespace details {
template <class RAIter, class Distance>
void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {
it += n;
}
template <class BidirIter, class Distance>
void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {
if (n > 0) {
while (n--) ++it;
}
else {
while (n++) --it;
}
}
template <class InputIter, class Distance>
void advance(InputIter& it, Distance n, std::input_iterator_tag) {
while (n--) {
++it;
}
}
}
template <class Iter, class Distance>
void advance(Iter& it, Distance n) {
details::advance(it, n,
typename std::iterator_traits<Iter>::iterator_category{} );
}
Gli argomenti std::XY_iterator_tag
dei details::advance
sovraccaricati details::advance
funzioni details::advance
sono parametri di funzione non utilizzati. L'effettiva implementazione non ha importanza (in realtà è completamente vuota). Il loro unico scopo è quello di consentire al compilatore di selezionare un sovraccarico in base a quali details::advance
classe tag details::advance
viene chiamato con.
In questo esempio, advance
utilizza la metafunzione iterator_traits<T>::iterator_category
che restituisce una delle classi iterator_tag
, a seconda del tipo effettivo di Iter
. Un oggetto predefinito di iterator_category<Iter>::type
consente quindi al compilatore di selezionare uno dei diversi overload dei details::advance
. (È probabile che questo parametro di funzione sia completamente ottimizzato, poiché è un oggetto costruito di default di una struct
vuota e mai usato).
L'invio di tag può fornire codice molto più facile da leggere rispetto agli equivalenti che utilizzano SFINAE e enable_if
.
Nota: mentre C ++ 17 if constexpr
può semplificare l'implementazione di advance
in particolare, non è adatto per implementazioni aperte a differenza di dispatching di tag.
Rileva se l'espressione è valida
È possibile rilevare se un operatore o una funzione possono essere chiamati su un tipo. Per verificare se una classe ha un sovraccarico di std::hash
, si può fare questo:
#include <functional> // for std::hash
#include <type_traits> // for std::false_type and std::true_type
#include <utility> // for std::declval
template<class, class = void>
struct has_hash
: std::false_type
{};
template<class T>
struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())>
: std::true_type
{};
Da C ++ 17, std::void_t
può essere usato per semplificare questo tipo di costrutto
#include <functional> // for std::hash
#include <type_traits> // for std::false_type, std::true_type, std::void_t
#include <utility> // for std::declval
template<class, class = std::void_t<> >
struct has_hash
: std::false_type
{};
template<class T>
struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > >
: std::true_type
{};
dove std::void_t
è definito come:
template< class... > using void_t = void;
Per rilevare se un operatore, come l' operator<
è definito, la sintassi è quasi la stessa:
template<class, class = void>
struct has_less_than
: std::false_type
{};
template<class T>
struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())>
: std::true_type
{};
Questi possono essere usati per usare una std::unordered_map<T>
se T
ha un sovraccarico per std::hash
, ma altrimenti tenta di usare una std::map<T>
:
template <class K, class V>
using hash_invariant_map = std::conditional_t<
has_hash<K>::value,
std::unordered_map<K, V>,
std::map<K,V>>;
Calcolo della potenza con C ++ 11 (e versioni successive)
Con C ++ 11 e calcoli superiori in fase di compilazione può essere molto più facile. Ad esempio, il calcolo della potenza di un determinato numero in fase di compilazione seguirà:
template <typename T>
constexpr T calculatePower(T value, unsigned power) {
return power == 0 ? 1 : value * calculatePower(value, power-1);
}
La parola chiave constexpr
è responsabile del calcolo della funzione nel tempo di compilazione, quindi e solo dopo, quando tutti i requisiti per questo verranno soddisfatti (per ulteriori informazioni consultare il riferimento alla parola chiave constexpr), ad esempio tutti gli argomenti devono essere noti al momento della compilazione.
Nota: in C ++ 11 la funzione constexpr
deve comporre solo da un'istruzione return.
Vantaggi: confrontando questo metodo con il metodo standard di calcolo del tempo di compilazione, questo metodo è utile anche per i calcoli del runtime. Significa che se gli argomenti della funzione non sono noti al momento della compilazione (ad esempio, il valore e la potenza sono dati come input dall'utente), la funzione viene eseguita in un tempo di compilazione, quindi non è necessario duplicare un codice (come sarebbe forzato in standard più vecchi di C ++).
Per esempio
void useExample() {
constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,
// as both arguments are known at compilation time
// and used for a constant expression.
int value;
std::cin >> value;
int runtimeCalculated = calculatePower(value, 3); // runtime calculated,
// because value is known only at runtime.
}
Un altro modo per calcolare la potenza in fase di compilazione può utilizzare l'espressione della piega come segue:
#include <iostream>
#include <utility>
template <class T, T V, T N, class I = std::make_integer_sequence<T, N>>
struct power;
template <class T, T V, T N, T... Is>
struct power<T, V, N, std::integer_sequence<T, Is...>> {
static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));
};
int main() {
std::cout << power<int, 4, 2>::value << std::endl;
}
Distinzione manuale dei tipi quando viene dato qualsiasi tipo T
Quando si implementa SFINAE utilizzando std::enable_if
, è spesso utile avere accesso ai modelli di supporto che determinano se un determinato tipo T
corrisponde a un insieme di criteri.
Per aiutarci, lo standard fornisce già due tipi analogici a true
e false
che sono std::true_type
e std::false_type
.
L'esempio seguente mostra come rilevare se un tipo T
è un puntatore oppure no, il modello is_pointer
il comportamento std::is_pointer
standard std::is_pointer
:
template <typename T>
struct is_pointer_: std::false_type {};
template <typename T>
struct is_pointer_<T*>: std::true_type {};
template <typename T>
struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }
Esistono tre passaggi nel codice precedente (a volte ne occorrono solo due):
La prima dichiarazione di
is_pointer_
è il caso predefinito ed eredita dastd::false_type
. Il caso predefinito dovrebbe sempre ereditare dastd::false_type
poiché è analogo a una "condizionefalse
".La seconda dichiarazione specializza il modello
is_pointer_
per il puntatoreT*
senza preoccuparsi di cosa sia realmenteT
Questa versione eredita dastd::true_type
.La terza dichiarazione (quella reale) rimuove semplicemente tutte le informazioni non necessarie da
T
(in questo caso rimuoviamovolatile
qualificatoriconst
evolatile
) e quindi ricade su una delle due dichiarazioni precedenti.
Poiché is_pointer<T>
è una classe, per accedere al suo valore è necessario:
- Use
::value
, ad esempiois_pointer<int>::value
-value
è un membro di classe statico di tipobool
ereditato dastd::true_type
ostd::false_type
; - Costruisci un oggetto di questo tipo, ad esempio
is_pointer<int>{}
- Funziona perchéstd::is_pointer
eredita il suo costruttore predefinito dastd::true_type
ostd::false_type
(che hanno costruttoriconstexpr
) e siastd::true_type
chestd::false_type
hannoconstexpr
operatori di conversione abool
.
È buona abitudine fornire "template helper helper" che consentono di accedere direttamente al valore:
template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
In C ++ 17 e versioni successive, la maggior parte dei modelli di helper fornisce già una versione _v
, ad esempio:
template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;
If-then-else
Il tipo std::conditional
nell'intestazione della libreria standard <type_traits>
può selezionare un tipo o l'altro, in base a un valore booleano in fase di compilazione:
template<typename T>
struct ValueOrPointer
{
typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};
Questa struttura contiene un puntatore a T
se T
è maggiore della dimensione di un puntatore, o T
stesso se è minore o uguale alla dimensione di un puntatore. Pertanto sizeof(ValueOrPointer)
sarà sempre <= sizeof(void*)
.
Min / Max generico con conteggio di argomenti variabili
È possibile scrivere una funzione generica (ad esempio min
) che accetta vari tipi numerici e un numero arbitrario di argomenti mediante meta-template template. Questa funzione dichiara un min
per due argomenti e in modo ricorsivo per ulteriori.
template <typename T1, typename T2>
auto min(const T1 &a, const T2 &b)
-> typename std::common_type<const T1&, const T2&>::type
{
return a < b ? a : b;
}
template <typename T1, typename T2, typename ... Args>
auto min(const T1 &a, const T2 &b, const Args& ... args)
-> typename std::common_type<const T1&, const T2&, const Args& ...>::type
{
return min(min(a, b), args...);
}
auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);