C++
Metaprogrammierung
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
.
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.
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;
}
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:
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:
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 .
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
{};
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.
}
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):
Die erste Deklaration von
is_pointer_
ist der Standardfall und erbt vonstd::false_type
. Der Standardfall sollte immer vonstd::false_type
erben, da er einer "false
Bedingung" entspricht.In der zweiten Deklaration wird das
is_pointer_
Template für den ZeigerT*
ohne sich darum zu kümmern, wasT
wirklich ist. Diese Version erbt vonstd::true_type
.Die dritte Deklaration (die echte) entfernt einfach alle unnötigen Informationen aus
T
(in diesem Fall entfernen wirconst
undvolatile
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 Typbool
das vonstd::true_type
oderstd::false_type
. - Konstruieren Sie ein Objekt dieses Typs, z. B.
is_pointer<int>{}
- Dies funktioniert, weilstd::is_pointer
seinen Standardkonstruktor vonstd::true_type
oderstd::false_type
(die überconstexpr
Konstruktoren verfügen) sowiestd::true_type
undstd::false_type
std::true_type
std::false_type
hatconstexpr
Konvertierungsoperatoren inbool
.
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;
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
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
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);