C++
metaprogrammering
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
.
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.
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;
}
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:
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:
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.
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
{};
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.
}
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å):
Den första deklarationen av
is_pointer_
är standardfallet och ärver frånstd::false_type
. Standardfallet ska alltid ärva frånstd::false_type
eftersom det är analogt med ett "false
tillstånd".Den andra deklarationen specialiserar
is_pointer_
mallen för pekarenT*
utan att bry sig om vadT
egentligen är. Den här versionen ärver frånstd::true_type
.Den tredje deklarationen (den verkliga) tar helt enkelt bort all onödig information från
T
(i detta fall tar vi bortconst
ochvolatile
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 typenbool
ärvs frånstd::true_type
ellerstd::false_type
; - Konstruera ett objekt av denna typ, t ex
is_pointer<int>{}
- Detta fungerar eftersomstd::is_pointer
ärver dess standard konstruktörs frånstd::true_type
ellerstd::false_type
(vilka harconstexpr
konstruktörer) och bådastd::true_type
ochstd::false_type
harconstexpr
konverteringsoperatörer tillbool
.
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;
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
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
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);