C++
metaprogramming
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
.
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.
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;
}
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:
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:
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.
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
{};
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.
}
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):
De eerste verklaring van
is_pointer_
is het standaardgeval en erft vanstd::false_type
. Het standaardgeval moet altijd erven vanstd::false_type
omdat het analoog is aan een "false
voorwaarde".De tweede verklaring specialiseert het
is_pointer_
sjabloon voor pointerT*
zonder zich druk te maken om watT
echt is. Deze versie neemt vanstd::true_type
.De derde verklaring (de echte) verwijdert eenvoudig alle onnodige informatie uit
T
(in dit geval verwijderen weconst
envolatile
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 typebool
overgenomen vanstd::true_type
ofstd::false_type
; - Construeer een object van dit type, bijv.
is_pointer<int>{}
- Dit werkt omdatstd::is_pointer
zijn standaardconstructorstd::true_type
vanstd::true_type
ofstd::false_type
(dieconstexpr
constructors hebben) en zowelstd::true_type
enstd::false_type
hebbenconstexpr
conversie-operators voorbool
.
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;
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
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
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);