Suche…


Einführung

In C ++ bezieht sich Metaprogrammierung auf die Verwendung von Makros oder Vorlagen zum Generieren von Code zur Kompilierzeit.

Im Allgemeinen werden Makros in dieser Rolle nicht gern gesehen, und Vorlagen werden bevorzugt, obwohl sie nicht so generisch sind.

Bei der Template-Metaprogrammierung werden häufig Berechnungen zur Kompilierzeit verwendet, constexpr über Templates oder über constexpr Funktionen, um die constexpr zu erreichen. constexpr -Berechnungen sind jedoch keine Metaprogrammierung per se.

Bemerkungen

Metaprogrammierung (oder genauer: Metaprogrammierung von Vorlagen) ist die Praxis, Vorlagen zum Erstellen von Konstanten, Funktionen oder Datenstrukturen zur Kompilierzeit zu verwenden. Dies ermöglicht, dass Berechnungen einmal zur Kompilierungszeit und nicht zu jeder Laufzeit ausgeführt werden.

Berechnungsfaktoren

Factorials können zur Kompilierzeit unter Verwendung von Schablonenmetaprogrammiertechniken berechnet werden.

#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 ist eine Struktur, aber in der Metaprogrammierung von Vorlagen wird sie als Meta-Funktion der Vorlage behandelt. Konventionell werden Vorlagen-Metafunktionen ausgewertet, indem ein bestimmtes Member überprüft wird, entweder ::type auf Metafunktionen, die zu Typen führen, oder ::value für Metafunktionen, die Werte generieren.

Im obigen Code evaluieren wir die factorial Metafunktion, indem wir die Vorlage mit den Parametern, die wir übergeben möchten, instantiieren und ::value , um das Ergebnis der Bewertung zu erhalten.

Die Metafunktion selbst ist darauf angewiesen, dieselbe Metafunktion rekursiv mit kleineren Werten zu instanziieren. Die factorial<0> Spezialisierung factorial<0> repräsentiert die Beendigungsbedingung. Die Metaprogrammierung von Vorlagen hat die meisten Einschränkungen einer funktionalen Programmiersprache. Rekursion ist also das primäre Konstrukt "Schleife".

Da Vorlagen-Metafunktionen zur Kompilierzeit ausgeführt werden, können ihre Ergebnisse in Kontexten verwendet werden, die Werte für die Kompilierungszeit erfordern. Zum Beispiel:

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

Automatische Arrays müssen eine für die Kompilierzeit definierte Größe haben. Das Ergebnis einer Metafunktion ist eine Konstante zur Kompilierzeit, die hier verwendet werden kann.

Einschränkung : Die meisten Compiler erlauben keine Rekursionstiefe über ein Limit hinaus. Beispielsweise hängt die Rekursion von g++ Compiler standardmäßig von 256 Stufen ab. Im Fall von g++ kann der Programmierer die Rekursionstiefe mit der Option -ftemplate-depth-X .

C ++ 11

Seit C ++ 11 kann die Vorlage std::integral_constant für diese Art von Vorlagenberechnung verwendet werden:

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

Darüber constexpr Funktionen von constexpr zu einer saubereren Alternative.

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

Der Rumpf von factorial() wird als einzelne Anweisung geschrieben, da constexpr Funktionen in C ++ 11 nur eine recht begrenzte Teilmenge der Sprache verwenden können.

C ++ 14

Seit C ++ 14 wurden viele Einschränkungen für constexpr Funktionen constexpr und sie können jetzt viel bequemer geschrieben werden:

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

Oder auch:

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

Seit C ++ 17 kann man den fold-Ausdruck verwenden, um die Fakultät zu berechnen:

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

Iteration über ein Parameterpaket

Häufig müssen wir für jedes Element in einem variadischen Vorlagenparameterpaket eine Operation ausführen. Es gibt viele Möglichkeiten, dies zu tun, und die Lösungen lassen sich mit C ++ 17 leichter lesen und schreiben. Angenommen, wir möchten einfach jedes Element in einer Packung drucken. Die einfachste Lösung ist die Wiederholung:

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

Wir können stattdessen den Expander-Trick verwenden, um das Streaming in einer einzigen Funktion auszuführen. Dies hat den Vorteil, dass keine zweite Überlastung erforderlich ist, hat jedoch den Nachteil, dass die Lesbarkeit weniger als der Stern ist:

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

Eine Erklärung, wie das funktioniert, finden Sie in der ausgezeichneten Antwort von TC .

C ++ 17

Mit C ++ 17 verfügen wir über zwei leistungsstarke neue Tools, um dieses Problem zu lösen. Der erste ist ein Falten-Ausdruck:

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

Und das zweite ist if constexpr , mit dem wir unsere ursprüngliche rekursive Lösung in einer einzigen Funktion schreiben können:

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

Iteration mit std :: integer_sequence

Seit C ++ 14 stellt der Standard die Klassenvorlage bereit

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

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

und eine generierende Metafunktion dafür:

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

Während dies in C ++ 14 Standard ist, kann dies mit C ++ 11-Tools implementiert werden.


Wir können dieses Tool verwenden, um eine Funktion mit einem std::tuple von Argumenten aufzurufen (standardisiert 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)

Tag-Versand

Eine einfache Möglichkeit, zwischen Funktionen zur Kompilierzeit zu wählen, besteht darin, eine Funktion an ein überladenes Funktionspaar zu übergeben, das ein Tag als ein (normalerweise das letzte) Argument nimmt. Um beispielsweise std::advance() zu implementieren, können wir die Iteratorkategorie abschicken:

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

Die Argumente std::XY_iterator_tag der überladenen details::advance Funktionen sind nicht verwendete Funktionsparameter. Die tatsächliche Implementierung spielt keine Rolle (eigentlich ist sie völlig leer). Ihr einziger Zweck besteht darin, dem Compiler zu ermöglichen, eine Überladung auszuwählen, die darauf basiert, mit welcher Tag-Klasse details::advance aufgerufen wird.

In diesem Beispiel advance verwendet die iterator_traits<T>::iterator_category metafunction , die eine der zurück iterator_tag Klassen, auf dem tatsächlichen Typ abhängig Iter . Ein standardmäßig erstelltes Objekt des iterator_category<Iter>::type lässt den Compiler dann eine der verschiedenen Überladungen von details::advance auswählen. (Dieser Funktionsparameter wird wahrscheinlich vollständig wegoptimiert, da er ein standardmäßig erstelltes Objekt einer leeren struct und niemals verwendet wird.)

Durch das Tag-Dispatching erhalten Sie Code, der viel einfacher zu lesen ist als die Äquivalente, die SFINAE und enable_if .

Anmerkung: if constexpr C ++ 17 die Implementierung von advance insbesondere durch if constexpr vereinfacht, ist es im Gegensatz zum Tag-Dispatching nicht für offene Implementierungen geeignet.

Ermitteln Sie, ob der Ausdruck gültig ist

Es kann ermittelt werden, ob ein Operator oder eine Funktion für einen Typ aufgerufen werden kann. Um zu testen, ob eine Klasse eine Überladung von std::hash , können Sie std::hash tun:

#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

Seit C ++ 17 kann std::void_t verwendet werden, um diesen Konstrukttyp zu vereinfachen

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

Dabei ist std::void_t definiert als:

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

Um festzustellen, ob ein Operator wie operator< definiert ist, ist die Syntax fast gleich:

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

Diese können verwendet werden, um eine std::unordered_map<T> wenn T eine Überladung für std::hash , aber ansonsten versuchen Sie eine 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>>;    

Rechenleistung mit C ++ 11 (und höher)

Mit C ++ 11 und höher können Berechnungen zur Kompilierzeit wesentlich einfacher sein. Zum Beispiel wird die Leistung einer bestimmten Anzahl zur Kompilierzeit berechnet:

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

Das Schlüsselwort constexpr ist für die Berechnung der Funktion in der Kompilierungszeit verantwortlich. Dann und nur dann, wenn alle Voraussetzungen dafür erfüllt sind (siehe mehr unter constexpr-Schlüsselwortreferenz), müssen beispielsweise alle Argumente zur Kompilierzeit bekannt sein.

Hinweis: In C ++ 11 darf die constexpr Funktion nur aus einer return-Anweisung bestehen.

Vorteile: Vergleicht man dies mit der Standardmethode der Kompilierzeitberechnung, ist diese Methode auch für Laufzeitberechnungen hilfreich. Das bedeutet, dass, wenn die Argumente der Funktion zum Zeitpunkt der Kompilierung nicht bekannt sind (z. B. Wert und Leistung als Eingabe über den Benutzer angegeben werden), die Funktion in einer Kompilierungszeit ausgeführt wird, so dass kein Code kopiert werden muss (wie wir würde in älteren Standards von C ++ erzwungen werden).

Z.B

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

Eine andere Methode zum Berechnen der Leistung zum Kompilierungszeitpunkt kann den fold-Ausdruck folgendermaßen verwenden:

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

Manuelle Unterscheidung der Typen bei gegebenem Typ T

Bei der Implementierung von SFINAE mithilfe von std::enable_if ist es häufig hilfreich, auf std::enable_if zuzugreifen, die bestimmen, ob ein bestimmter Typ T mit einem Satz von Kriterien übereinstimmt.

Um uns dabei zu helfen, sieht der Standard bereits zwei Typen Analog true und false , die sind std::true_type und std::false_type .

Das folgende Beispiel zeigt , wie zu erkennen , ob ein Typ T ein Zeiger ist oder nicht, die is_pointer Vorlage das Verhalten der Standard imitieren std::is_pointer Helfer:

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

Der obige Code besteht aus drei Schritten (manchmal benötigen Sie nur zwei):

  1. Die erste Deklaration von is_pointer_ ist der Standardfall und erbt von std::false_type . Der Standardfall sollte immer von std::false_type erben, da er einer " false Bedingung" entspricht.

  2. In der zweiten Deklaration wird das is_pointer_ Template für den Zeiger T* ohne sich darum zu kümmern, was T wirklich ist. Diese Version erbt von std::true_type .

  3. Die dritte Deklaration (die echte) entfernt einfach alle unnötigen Informationen aus T (in diesem Fall entfernen wir const und volatile Qualifiers) und greifen dann auf eine der beiden vorherigen Deklarationen zurück.

Da is_pointer<T> eine Klasse ist, müssen Sie zum Zugriff auf den Wert entweder:

  • Verwenden Sie ::value , z. B. is_pointer<int>::value - value ist ein statischer Klassenmitglied vom Typ bool das von std::true_type oder std::false_type .
  • Konstruieren Sie ein Objekt dieses Typs, z. B. is_pointer<int>{} - Dies funktioniert, weil std::is_pointer seinen Standardkonstruktor von std::true_type oder std::false_type (die über constexpr Konstruktoren verfügen) sowie std::true_type und std::false_type std::true_type std::false_type hat constexpr Konvertierungsoperatoren in bool .

Es ist eine gute Angewohnheit, "Helfer-Helfer-Vorlagen" bereitzustellen, mit denen Sie direkt auf den Wert zugreifen können:

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

In C ++ 17 und höher bieten die meisten Helper-Templates bereits eine _v Version, z.

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

Wenn-dann-sonst

C ++ 11

Der Typ std::conditional im Header der Standardbibliothek <type_traits> kann den einen oder den anderen Typ auswählen, basierend auf einem booleschen Wert der Kompilierungszeit:

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

Diese Struktur enthält einen Zeiger auf T wenn T größer als die Größe eines Zeigers ist, oder T selbst, wenn er kleiner oder gleich der Größe eines Zeigers ist. Deshalb ist sizeof(ValueOrPointer) immer <= sizeof(void*) .

Generisches Min / Max mit variabler Argumentanzahl

C ++ 11

Es ist möglich, eine generische Funktion (z. B. min ) zu schreiben, die verschiedene numerische Typen und die Anzahl beliebiger Argumente durch Template-Meta-Programmierung akzeptiert. Diese Funktion deklariert ein min für zwei Argumente und rekursiv für mehr.

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
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow