Zoeken…


Invoering

In C ++ verwijst metaprogrammering naar het gebruik van macro's of sjablonen om tijdens het compileren code te genereren.

Over het algemeen worden macro's in deze rol afgekeurd en hebben sjablonen de voorkeur, hoewel ze niet zo generiek zijn.

Sjabloon-metaprogrammering maakt vaak gebruik van compilatie- constexpr , hetzij via sjablonen of constexpr functies, om zijn doelen van het genereren van code te bereiken, compilatie- constexpr zijn echter op zichzelf geen metaprogrammering.

Opmerkingen

Metaprogramming (of meer specifiek, Template Metaprogramming) is het gebruik van sjablonen om tijdens het compileren constanten, functies of gegevensstructuren te maken. Hierdoor kunnen berekeningen eenmaal tijdens het compileren worden uitgevoerd in plaats van bij elke uitvoeringstijd.

Factorials berekenen

Factorials kunnen tijdens het compileren worden berekend met behulp van sjabloonmetaprogrammeringstechnieken.

#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 is een struct, maar in sjabloonmetaprogrammering wordt het behandeld als een sjabloonmetafunctie. Volgens afspraak worden sjabloonmetafuncties geëvalueerd door een bepaald lid te controleren, ofwel ::type voor metafuncties die in typen resulteren, of ::value voor metafuncties die waarden genereren.

In de bovenstaande code evalueren we de factorial metafunctie door de sjabloon te instantiëren met de parameters die we willen doorgeven, en door ::value te gebruiken om het resultaat van de evaluatie te krijgen.

De metafunctie zelf is afhankelijk van het recursief instantiëren van dezelfde metafunctie met kleinere waarden. De factorial<0> specialisatie vertegenwoordigt de beëindigende voorwaarde. Sjabloon metaprogrammering heeft de meeste beperkingen van een functionele programmeertaal , dus recursie is het primaire "looping" -construct.

Aangezien sjabloonmetafuncties tijdens het compileren worden uitgevoerd, kunnen hun resultaten worden gebruikt in contexten die compilatie-tijd vereisen. Bijvoorbeeld:

int my_array[factorial<5>::value];

Automatische arrays moeten een compilatie-tijd gedefinieerde grootte hebben. En het resultaat van een metafunctie is een compilatie-tijdconstante, dus deze kan hier worden gebruikt.

Beperking : de meeste compilers staan geen recursiediepte toe voorbij een limiet. Bijvoorbeeld, g++ compiler beperkt standaard recursie tot 256 niveaus. In het geval van g++ kan de programmeur de recursiediepte instellen met de optie -ftemplate-depth-X .

C ++ 11

Sinds C ++ 11 kan de sjabloon std::integral_constant worden gebruikt voor dit soort sjabloonberekening:

#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"
}

Bovendien worden constexpr functies een schoner alternatief.

#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';
}

De body van factorial() is geschreven als een enkele instructie omdat in C ++ 11 constexpr functies slechts een vrij beperkte subset van de taal kunnen gebruiken.

C ++ 14

Sinds C ++ 14 zijn veel beperkingen voor constexpr functies constexpr en kunnen deze nu veel gemakkelijker worden geschreven:

constexpr long long factorial(long long n)
{
  if (n == 0)
    return 1;
  else
    return n * factorial(n - 1);
}

Of zelfs:

constexpr long long factorial(int n)
{
  long long result = 1;
  for (int i = 1; i <= n; ++i) {
    result *= i;
  }
  return result;
}
C ++ 17

Sinds c ++ 17 kan men vouwexpressie gebruiken om faculteit te berekenen:

#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;
}

Itereren over een parameterpakket

Vaak moeten we een bewerking uitvoeren voor elk element in een variadisch sjabloonparameterpakket. Er zijn veel manieren om dit te doen en de oplossingen worden gemakkelijker te lezen en te schrijven met C ++ 17. Stel dat we gewoon elk element in een pakket willen afdrukken. De eenvoudigste oplossing is om te verplegen:

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...);
}

We kunnen in plaats daarvan de expander-truc gebruiken om alle streaming in één functie uit te voeren. Dit heeft het voordeel dat er geen tweede overbelasting nodig is, maar heeft het nadeel van minder dan uitstekende leesbaarheid:

C ++ 11
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    using expander = int[];
    (void)expander{0,
        (void(os << args), 0)...
    };
}

Zie het uitstekende antwoord van TC voor een uitleg over hoe dit werkt.

C ++ 17

Met C ++ 17 krijgen we twee krachtige nieuwe tools in ons arsenaal om dit probleem op te lossen. De eerste is een vouwuitdrukking:

template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    ((os << args), ...);
}

En de tweede is if constexpr , waarmee we onze oorspronkelijke recursieve oplossing in één functie kunnen schrijven:

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...);
    }
}

Itereren met std :: integer_sequence

Sinds C ++ 14 biedt de standaard de klassensjabloon

template <class T, T... Ints>
class integer_sequence;

template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;

en een genererende metafunctie daarvoor:

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

Hoewel dit standaard wordt geleverd in C ++ 14, kan dit worden geïmplementeerd met behulp van C ++ 11-tools.


We kunnen deze tool gebruiken om een functie aan te roepen met een std::tuple van argumenten (gestandaardiseerd in C ++ 17 als 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)

Tagverzending

Een eenvoudige manier om tijdens het compileren tussen functies te kiezen, is om een functie naar een overbelast paar functies te verzenden die een tag als één (meestal het laatste) argument gebruiken. Om bijvoorbeeld std::advance() te implementeren, kunnen we de iteratorcategorie verzenden:

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{} );
}

De argumenten std::XY_iterator_tag van de overbelaste details::advance functies zijn ongebruikte functieparameters. De daadwerkelijke implementatie doet er niet toe (eigenlijk is deze helemaal leeg). Hun enige doel is om de compiler een overbelasting te laten selecteren op basis van welke tagklasse- details::advance wordt aangeroepen.

In dit voorbeeld gebruikt advance de iterator_traits<T>::iterator_category metafunctie die een van de iterator_tag klassen retourneert, afhankelijk van het werkelijke type Iter . Een standaard-geconstrueerd object van het type iterator_category<Iter>::type laat de compiler vervolgens een van de verschillende overbelastingen met details::advance . (Deze functieparameter zal waarschijnlijk volledig worden geoptimaliseerd, omdat het een standaard geconstrueerd object van een lege struct en nooit wordt gebruikt.)

Tagverzending kan u code geven die veel gemakkelijker te lezen is dan de equivalenten die SFINAE en enable_if .

Opmerking: terwijl C ++ 17's if constexpr kan de uitvoering van vereenvoudiging van advance in het bijzonder, het is niet geschikt voor de open-implementaties in tegenstelling tot tag dispatching.

Detecteren of expressie geldig is

Het is mogelijk om te detecteren of een operator of functie op een type kan worden opgeroepen. Om te testen of een klasse een overbelasting van std::hash , kan men dit doen:

#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

Sinds C ++ 17 kan std::void_t worden gebruikt om dit type constructie te vereenvoudigen

#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
{};

waar std::void_t is gedefinieerd als:

template< class... > using void_t = void;

Om te detecteren of een operator, zoals operator< is gedefinieerd, is de syntaxis bijna hetzelfde:

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
{};

Deze kunnen worden gebruikt om een std::unordered_map<T> als T een overbelasting heeft voor std::hash , maar probeer anders een 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>>;    

Berekeningsvermogen met C ++ 11 (en hoger)

Met C ++ 11 en hoger kunnen berekeningen tijdens het compileren veel eenvoudiger zijn. Het berekenen van het vermogen van een bepaald nummer tijdens het compileren zal bijvoorbeeld het volgende zijn:

template <typename T>
constexpr T calculatePower(T value, unsigned power) {
    return power == 0 ? 1 : value * calculatePower(value, power-1);
}

Trefwoord constexpr is verantwoordelijk voor het berekenen van de functie in de compilatietijd, dan en alleen dan, wanneer aan alle vereisten hiervoor wordt voldaan (zie meer op constexpr trefwoordreferentie), bijvoorbeeld moeten alle argumenten bekend zijn tijdens het compileren.

Opmerking: in C ++ 11 moet de constexpr functie slechts uit één constexpr .

Voordelen: in vergelijking met de standaardmanier voor het compileren van tijd, is deze methode ook nuttig voor runtime-berekeningen. Het betekent dat als de argumenten van de functie niet bekend zijn op het moment van compilatie (bijv. Waarde en macht worden gegeven als invoer via gebruiker), dan de functie wordt uitgevoerd in een compilatietijd, dus het is niet nodig om een code te dupliceren (zoals we zou worden gedwongen in oudere normen van C ++).

Eg

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

Een andere manier om het vermogen te berekenen tijdens het compileren, kan als volgt gebruik maken van vouwuitdrukking:

#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;
}

Handmatig onderscheid van typen bij elk type T

Bij het implementeren van SFINAE met std::enable_if , is het vaak handig om toegang te hebben tot std::enable_if die bepalen of een bepaald type T overeenkomt met een set criteria.

Om ons daarbij te helpen, biedt de standaard al twee typen analoog aan true en false die std::true_type en std::false_type .

Het volgende voorbeeld laat zien hoe te detecteren of een type T een pointer is of niet, de is_pointer sjabloon bootst het gedrag van de standaard std::is_pointer helper na:

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> { }

Er zijn drie stappen in de bovenstaande code (soms heb je er maar twee nodig):

  1. De eerste verklaring van is_pointer_ is het standaardgeval en erft van std::false_type . Het standaardgeval moet altijd erven van std::false_type omdat het analoog is aan een " false voorwaarde".

  2. De tweede verklaring specialiseert het is_pointer_ sjabloon voor pointer T* zonder zich druk te maken om wat T echt is. Deze versie neemt van std::true_type .

  3. De derde verklaring (de echte) verwijdert eenvoudig alle onnodige informatie uit T (in dit geval verwijderen we const en volatile kwalificaties) en valt dan terug op een van de twee vorige verklaringen.

Omdat is_pointer<T> een klasse is, moet u om toegang te krijgen tot de waarde ervan:

  • Use ::value , bijv. is_pointer<int>::value - value is een statisch klasse-lid van het type bool overgenomen van std::true_type of std::false_type ;
  • Construeer een object van dit type, bijv. is_pointer<int>{} - Dit werkt omdat std::is_pointer zijn standaardconstructor std::true_type van std::true_type of std::false_type (die constexpr constructors hebben) en zowel std::true_type en std::false_type hebben constexpr conversie-operators voor bool .

Het is een goede gewoonte om 'helperhelpersjablonen' te bieden waarmee u rechtstreeks toegang hebt tot de waarde:

template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
C ++ 17

In C ++ 17 en hoger bieden de meeste _v al een _v versie, bijvoorbeeld:

template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;

Als dan anders

C ++ 11

Het type std::conditional in de standaardbibliotheekkop <type_traits> kan het ene type of het andere selecteren op basis van een compilatie-tijd booleaanse waarde:

template<typename T>
struct ValueOrPointer
{
    typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};

Deze structuur bevat een aanwijzer naar T als T groter is dan de grootte van een aanwijzer, of T zelf als deze kleiner is of gelijk is aan de grootte van een aanwijzer. Daarom is sizeof(ValueOrPointer) altijd <= sizeof(void*) .

Generiek Min / Max met variabel aantal argumenten

C ++ 11

Het is mogelijk om een generieke functie (bijvoorbeeld min ) te schrijven die verschillende numerieke typen en willekeurige argumenttelling accepteert met behulp van sjabloon-meta-programmering. Deze functie declareert een min voor twee argumenten en recursief voor meer.

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
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow