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 .

C ++ 11

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.

C ++ 14

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;
}
C ++ 17

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:

C ++ 11
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:

C ++ 11
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 .

C ++ 17

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
{};
C ++ 17

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.
}
C ++ 17

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):

  1. La prima dichiarazione di is_pointer_ è il caso predefinito ed eredita da std::false_type . Il caso predefinito dovrebbe sempre ereditare da std::false_type poiché è analogo a una "condizione false ".

  2. La seconda dichiarazione specializza il modello is_pointer_ per il puntatore T* senza preoccuparsi di cosa sia realmente T Questa versione eredita da std::true_type .

  3. La terza dichiarazione (quella reale) rimuove semplicemente tutte le informazioni non necessarie da T (in questo caso rimuoviamo volatile qualificatori const e volatile ) e quindi ricade su una delle due dichiarazioni precedenti.

Poiché is_pointer<T> è una classe, per accedere al suo valore è necessario:

  • Use ::value , ad esempio is_pointer<int>::value - value è un membro di classe statico di tipo bool ereditato da std::true_type o std::false_type ;
  • Costruisci un oggetto di questo tipo, ad esempio is_pointer<int>{} - Funziona perché std::is_pointer eredita il suo costruttore predefinito da std::true_type o std::false_type (che hanno costruttori constexpr ) e sia std::true_type che std::false_type hanno constexpr operatori di conversione a bool .

È 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;
C ++ 17

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

C ++ 11

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

C ++ 11

È 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);


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow