Sök…


Introduktion

I C ++ avser metaprogrammering användningen av makron eller mallar för att generera kod vid sammanställningstid.

I allmänhet är makron rynkade i denna roll och mallar föredras, även om de inte är lika generiska.

Mallmetaprogrammering använder ofta kompileringstidsberäkningar, vare sig det är via mallar eller constexpr funktioner, för att uppnå sina mål att generera kod, men kompileringstidsberäkningar är inte i sig själva metaprogrammering.

Anmärkningar

Metaprogrammering (eller mer specifikt Mall Metaprogramming) är praxis att använda mallar för att skapa konstanter, funktioner eller datastrukturer vid sammanställningstiden. Detta gör att beräkningar kan utföras en gång vid sammanställningstid snarare än vid varje körtid.

Beräkna fakultet

Factorials kan beräknas vid kompileringstid med hjälp av mallmetaprogrammeringstekniker.

#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 är en struktur, men i mallmetaprogrammering behandlas den som en mallmetafunktion. Genom konvention utvärderas mallmetafunktioner genom att kontrollera en viss medlem, antingen ::type för metafunktioner som resulterar i typer, eller ::value för metafunktioner som genererar värden.

I ovanstående kod, utvärderar vi factorial metafunction genom instansiera mallen med de parametrar som vi vill att passera, och med hjälp av ::value för att få resultatet av utvärderingen.

Metafunktionen förlitar sig på rekursivt instans av samma metafunktion med mindre värden. factorial<0> specialisering representerar avslutningsvillkoret. Mallmetaprogrammering har de flesta begränsningarna för ett funktionellt programmeringsspråk , så rekursion är den primära "looping" -konstruktionen.

Eftersom mallmetafunktioner utförs vid sammanställningstid kan deras resultat användas i sammanhang som kräver kompileringstidsvärden. Till exempel:

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

Automatiska matriser måste ha en definierad storlek för kompileringstiden. Och resultatet av en metafunktion är en sammanställningstidskonstant, så den kan användas här.

Begränsning : De flesta av kompilatorerna tillåter inte rekursionsdjup utöver en gräns. Exempelvis begränsar rekursion g++ -kompilerare till 256 nivåer. Vid g++ kan programmerare ställa in rekursionsdjup med -ftemplate-depth-X .

C ++ 11

Sedan C ++ 11 kan std::integral_constant mallen användas för denna typ av mallberäkning:

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

Dessutom blir constexpr funktioner ett renare alternativ.

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

Faktorns kropp factorial() är skriven som ett enda uttalande eftersom constexpr funktioner i C ++ 11 constexpr kan använda en ganska begränsad delmängd av språket.

C ++ 14

Sedan C ++ 14 har många begränsningar för constexpr funktioner tappats och de kan nu skrivas mycket mer bekvämt:

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

Eller ens:

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

Eftersom c ++ 17 kan man använda vikuttryck för att beräkna faktoriell:

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

Iterera över ett parameterpaket

Ofta måste vi utföra en operation över varje element i ett variadmallparameterpaket. Det finns många sätt att göra detta, och lösningarna blir lättare att läsa och skriva med C ++ 17. Anta att vi helt enkelt vill skriva ut alla element i ett paket. Den enklaste lösningen är att återkräva:

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

Vi kan istället använda expander-tricket för att utföra all strömning i en enda funktion. Detta har fördelen av att det inte behövs en andra överbelastning, men har nackdelen med mindre än stellär läsbarhet:

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

Se TC: s utmärkta svar för en förklaring av hur detta fungerar.

C ++ 17

Med C ++ 17 får vi två kraftfulla nya verktyg i vårt arsenal för att lösa detta problem. Den första är ett vikuttryck:

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

Och den andra är if constexpr , som gör att vi kan skriva vår ursprungliga rekursiva lösning i en enda funktion:

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

Iterating med std :: integer_sequence

Sedan C ++ 14 ger standarden klassmallen

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

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

och en genererande metafunktion för det:

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

Även om detta levereras som standard i C ++ 14, kan det implementeras med C ++ 11-verktyg.


Vi kan använda det här verktyget för att kalla en funktion med en std::tuple av argument (standardiserad i C ++ 17 som 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)

Taggssändning

Ett enkelt sätt att välja mellan funktioner vid kompileringstid är att skicka en funktion till ett överbelastat par funktioner som tar en tagg som ett (vanligtvis det sista) argumentet. Till exempel för att implementera std::advance() kan vi skicka till kategorin iterator:

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

Argumenten std::XY_iterator_tag för de överbelastade details::advance är oanvända funktionsparametrar. Den faktiska implementeringen spelar ingen roll (den är faktiskt helt tom). Deras enda syfte är att låta kompilatorn välja en överbelastning baserad på vilken etikettklassinformation details::advance kallas med.

I det här exemplet använder advance iterator_traits<T>::iterator_category metafunktion som returnerar en av iterator_tag klasserna, beroende på den faktiska typen av Iter . Ett standardkonstruerat objekt av iterator_category<Iter>::type låter sedan kompilatorn välja en av de olika överbelastningarna med details::advance . (Denna funktionsparameter är troligen helt optimerad bort, eftersom den är ett standardkonstruerat objekt för en tom struct och aldrig används.)

Taggsändning kan ge dig kod som är mycket lättare att läsa än motsvarigheterna med SFINAE och enable_if .

Observera: medan C ++ 17: er if constexpr kan förenkla genomförandet av advance i synnerhet, är det inte lämpligt för öppna implementeringar till skillnad från taggssändning.

Upptäck om uttrycket är giltigt

Det är möjligt att upptäcka om en operatör eller funktion kan anropas på en typ. För att testa om en klass har en överbelastning av std::hash , kan man göra detta:

#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

Eftersom C ++ 17 kan std::void_t användas för att förenkla denna typ av konstruktion

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

där std::void_t definieras som:

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

För att detektera om en operatör, t.ex. operator< är definierad, är syntaxen nästan densamma:

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

Dessa kan användas för att använda en std::unordered_map<T> om T har en överbelastning för std::hash , men annars försöker använda en 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>>;    

Beräkningseffekt med C ++ 11 (och högre)

Med C ++ 11 och högre beräkningar vid sammanställningstiden kan vara mycket enklare. Exempelvis kommer beräkningen av effekten för ett visst nummer vid sammanställningstiden att följa:

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

Nyckelord constexpr ansvarar för att beräkna funktion i sammanställningstid, då och då, när alla krav för detta kommer att uppfyllas (se mer vid constexpr-sökordreferens), till exempel måste alla argument vara kända vid sammanställningstiden.

Obs: I C ++ 11 måste constexpr funktionen bara komponera från ett returrätt.

Fördelar: Jämför detta med det vanliga sättet att sammanställa tidsberäkning, den här metoden är också användbar för runtime-beräkningar. Det innebär att om argumenten för funktionen inte är kända vid sammanställningstiden (t.ex. värde och effekt ges som inmatning via användare), så körs funktionen i en sammanställningstid, så det finns inget behov av att kopiera en kod (som vi skulle tvingas i äldre standarder för C ++).

T.ex

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

Ett annat sätt att beräkna effekt vid sammanställningstid kan använda vikuttrycket på följande sätt:

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

Manuell åtskillnad mellan typer när det ges någon typ T

Vid implementering av SFINAE med std::enable_if är det ofta användbart att ha tillgång till hjälpmallar som avgör om en given typ T matchar en uppsättning kriterier.

För att hjälpa oss med det tillhandahåller standarden redan två typer av analog till true och false som är std::true_type och std::false_type .

Följande exempel visar hur man upptäcker om en typ T är en pekare eller inte, is_pointer mallen efterliknar beteendet hos standarden std::is_pointer helper:

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

Det finns tre steg i koden ovan (ibland behöver du bara två):

  1. Den första deklarationen av is_pointer_ är standardfallet och ärver från std::false_type . Standardfallet ska alltid ärva från std::false_type eftersom det är analogt med ett " false tillstånd".

  2. Den andra deklarationen specialiserar is_pointer_ mallen för pekaren T* utan att bry sig om vad T egentligen är. Den här versionen ärver från std::true_type .

  3. Den tredje deklarationen (den verkliga) tar helt enkelt bort all onödig information från T (i detta fall tar vi bort const och volatile kval) och faller sedan tillbaka till en av de två tidigare deklarationerna.

Eftersom is_pointer<T> är en klass, för att komma åt dess värde måste du antingen:

  • Använd ::value , t.ex. is_pointer<int>::value - value är en statisk klassmedlem av typen bool ärvs från std::true_type eller std::false_type ;
  • Konstruera ett objekt av denna typ, t ex is_pointer<int>{} - Detta fungerar eftersom std::is_pointer ärver dess standard konstruktörs från std::true_type eller std::false_type (vilka har constexpr konstruktörer) och båda std::true_type och std::false_type har constexpr konverteringsoperatörer till bool .

Det är en bra vana att tillhandahålla "helper helper mallar" som ger dig direkt tillgång till värdet:

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

I C ++ 17 och senare har de flesta hjälpmallar redan en _v version, t.ex.

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

Om då annars

C ++ 11

Typen std::conditional i standardbibliotekets rubrik <type_traits> kan välja en typ eller den andra, baserat på ett booleskt värde för kompileringstid:

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

Denna struktur innehåller en pekare till T om T är större än storleken på en pekare, eller T själv om den är mindre eller lika med en pekarens storlek. Därför kommer sizeof(ValueOrPointer) alltid att vara <= sizeof(void*) .

Generisk Min / Max med variabelt argumentantal

C ++ 11

Det är möjligt att skriva en generisk funktion (till exempel min ) som accepterar olika siffertyper och godtyckliga argumenträkningar med metaprogrammering av mallar. Denna funktion förklarar en min för två argument och rekursivt för mer.

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
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow