Programmering

Introduktion til metaprogrammering i C ++

Forrige 1 2 3 Side 3 Side 3 af 3
  • Tilstandsvariabler: Skabelonparametrene
  • Loop-konstruktioner: Gennem rekursion
  • Valg af henrettelsesstier: Ved at bruge betingede udtryk eller specialiseringer
  • Heltalsregning

Hvis der ikke er grænser for mængden af ​​rekursive instantieringer og antallet af tilladte variabler, er dette tilstrækkeligt til at beregne alt, hvad der kan beregnes. Det er dog muligvis ikke praktisk at gøre det ved hjælp af skabeloner. Desuden, fordi skabelon-instantiering kræver betydelige kompileringsressourcer, bremser omfattende rekursiv instantiering hurtigt en compiler eller udtømmer endda de tilgængelige ressourcer. C ++ -standarden anbefaler, men pålægger ikke, at 1.024 niveauer af rekursive instantieringer tillades som et minimum, hvilket er tilstrækkeligt til de fleste (men bestemt ikke alle) skabelonmetaprogrammeringsopgaver.

I praksis skal skabelonmetaprogrammer således bruges sparsomt. Der er dog et par situationer, hvor de er uerstattelige som et værktøj til at implementere praktiske skabeloner. Især kan de undertiden være skjult i indvendige af mere konventionelle skabeloner for at presse mere ydelse ud af kritiske algoritmeimplementeringer.

Rekursiv instantiering versus rekursive skabelonargumenter

Overvej følgende rekursive skabelon:

skabelon struktur fordobling {}; skabelon struktur Fejl {ved hjælp af LongType = Dobbeltgør; }; skabelon struktur Fejl {ved hjælp af LongType = dobbelt; }; Problemer :: LongType ouch;

Brugen af Fejl :: LongType udløser ikke kun den rekursive instantiering af Problemer, Problemer, …, Problemer, men det instantieres også Dobbeltgør over stadig mere komplekse typer. Tabellen illustrerer, hvor hurtigt den vokser.

Væksten af Fejl :: LongType

 
Skriv aliasUnderliggende type
Fejl :: LongTypedobbelt
Fejl :: LongTypeDobbeltgør
Fejl :: LongTypeDobbeltgør<>

Dobbeltgør>

Fejl :: LongTypeDobbeltgør<>

Doublify>,

   <>

Dobbeltgør >>

Som tabellen viser, er kompleksiteten af ​​typebeskrivelsen af ​​udtrykket Fejl :: LongType vokser eksponentielt med N. Generelt understreger en sådan situation en C ++ - kompilator endnu mere end rekursive instantieringer, der ikke involverer rekursive skabelonargumenter. Et af problemerne her er, at en kompilator holder en gengivelse af det manglede navn for typen. Dette manglede navn koder den nøjagtige skabelonspecialisering på en eller anden måde, og tidlige C ++ - implementeringer brugte en kodning, der er nogenlunde proportional med længden af ​​skabelon-id'et. Disse kompilatorer brugte derefter godt over 10.000 tegn til Fejl :: LongType.

Nyere C ++ - implementeringer tager højde for det faktum, at indlejrede skabelon-id'er er ret almindelige i moderne C ++ - programmer og bruger kloge komprimeringsteknikker for at reducere væksten i navnekodning betydeligt (for eksempel et par hundrede tegn Fejl :: LongType). Disse nyere compilere undgår også at generere et manglet navn, hvis der faktisk ikke er behov for noget, fordi der ikke genereres nogen lavt niveau kode til skabeloninstansen. Stadig alt andet lige er det sandsynligvis at foretrække at organisere rekursiv instantiering på en sådan måde, at skabelonargumenter ikke også behøver at være nestet rekursivt.

Tællingsværdier versus statiske konstanter

I de tidlige dage af C ++ var optællingsværdier den eneste mekanisme til at skabe "ægte konstanter" (kaldet konstant-udtryk) som navngivne medlemmer i klassedeklarationer. Med dem kan du f.eks. Definere en Pow3 metaprogram til beregning af beføjelser på 3 som følger:

meta / pow3enum.hpp // primær skabelon til beregning af 3 til Nth skabelon struct Pow3 {enum {værdi = 3 * Pow3 :: værdi}; }; // fuld specialisering for at afslutte rekursionsskabelonen struct Pow3 {enum {value = 1}; };

Standardiseringen af ​​C ++ 98 introducerede konceptet med klassiske initialiserere med konstant konstant, så Pow3-metaprogrammet kunne se ud som følger:

meta / pow3const.hpp // primær skabelon til beregning af 3 til Nth skabelon struct Pow3 {statisk int const værdi = 3 * Pow3 :: værdi; }; // fuld specialisering for at afslutte rekursionsskabelonen struct Pow3 {statisk int const værdi = 1; };

Der er dog en ulempe ved denne version: Statiske konstante medlemmer er værdier. Så hvis du har en erklæring som f.eks

ugyldig foo (int const &);

og du sender det resultatet af et metaprogram:

foo (Pow3 :: værdi);

en kompilator skal bestå adresse af Pow3 :: værdi, og det tvinger kompilatoren til at instantiere og tildele definitionen til det statiske medlem. Som et resultat er beregningen ikke længere begrænset til en ren "kompileringstid" -effekt.

Tællingsværdier er ikke værdier (dvs. de har ikke en adresse). Så når du sender dem som reference, bruges der ingen statisk hukommelse. Det er næsten nøjagtigt som om du har bestået den beregnede værdi som en bogstavelig.

C ++ 11 blev imidlertid introduceret konstexpr medlemmer af statiske data, og disse er ikke begrænset til integrerede typer. De løser ikke ovennævnte adressespørgsmål, men på trods af den mangel er de nu en almindelig måde at producere resultater af metaprogrammer på. De har fordelen af ​​at have en korrekt type (i modsætning til en kunstig enumtype), og den type kan udledes, når det statiske medlem erklæres med den automatiske typespecifikator. C ++ 17 tilføjede indbyggede statiske datamedlemmer, som løser ovennævnte adresseproblem og kan bruges sammen med konstexpr.

Metaprogrammering historie

Det tidligste dokumenterede eksempel på et metaprogram var af Erwin Unruh, der derefter repræsenterede Siemens i C ++ - standardiseringsudvalget. Han bemærkede den beregningsfulde fuldstændighed af skabelon-instantieringsprocessen og demonstrerede sit punkt ved at udvikle det første metaprogram. Han brugte Metaware-kompilatoren og lokket den til at udsende fejlmeddelelser, der ville indeholde successive primtal. Her er koden, der blev cirkuleret på et C ++ -udvalgsmøde i 1994 (ændret, så det nu kompileres på standardkonformatorer):

meta / unruh.cpp // beregning af primtal (// ændret med tilladelse fra original fra 1994 af Erwin Unruh) skabelon struct is_prime {enum ((p% i) && is_prime2? p: 0), i-1> :: pri); }; skabelonstruktur is_prime {enum {pri = 1}; }; skabelonstruktur is_prime {enum {pri = 1}; }; skabelon struktur D {D (ugyldig *); }; skabelon struct CondNull {static int const value = i; }; skabelon struct CondNull {statisk ugyldig * værdi; }; ugyldigt * CondNull :: værdi = 0; skabelon struct Prime_print {

// primær skabelon til sløjfe til udskrivning af primtal Prime_print a; enum {pri = is_prime :: pri}; ugyldigt f () {D d = CondNull :: værdi;

// 1 er en fejl, 0 er fin a.f (); }}; skabelon struktur Prime_print {

// fuld specialisering for at afslutte loop enum {pri = 0}; ugyldigt f () {D d = 0; }; }; #ifndef LAST #define LAST 18 #endif int main () {Prime_print a; a.f (); }

Hvis du kompilerer dette program, udskriver compileren fejlmeddelelser, når, i Prime_print :: f ()initialiseringen af ​​d mislykkes. Dette sker, når den oprindelige værdi er 1, fordi der kun er en konstruktør til ugyldig *, og kun 0 har en gyldig konvertering til ugyldig*. For eksempel på en compiler får vi (blandt flere andre meddelelser) følgende fejl:

unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: fejl: ingen levedygtig konvertering fra 'const int' til d'

Bemærk: Da fejlhåndtering i kompilatorer er forskellig, stopper nogle compilere muligvis efter udskrivning af den første fejlmeddelelse.

Begrebet C ++ - skabelonmetaprogrammering som et seriøst programmeringsværktøj blev først populært (og noget formaliseret) af Todd Veldhuizen i sin artikel "Brug af C ++ - skabelonmetaprogrammer." Veldhuizen arbejde med Blitz ++ (et numerisk array-bibliotek til C ++) introducerede også mange forbedringer og udvidelser til metaprogrammering (og til ekspressionskabelonteknikker).

Både den første udgave af denne bog og Andrei Alexandrescu Moderne C ++ design bidraget til en eksplosion af C ++ - biblioteker, der udnytter skabelonbaseret metaprogrammering ved at katalogisere nogle af de grundlæggende teknikker, der stadig er i brug i dag. Boost-projektet var medvirkende til at bringe orden på denne eksplosion. Tidligt introducerede det MPL (metaprogramming library), som definerede en ensartet ramme for skriv metaprogrammering blev populær også gennem David Abrahams og Aleksey Gurtovoys bog C ++ skabelonmetaprogrammering.

Yderligere vigtige fremskridt er gjort af Louis Dionne med at gøre metaprogrammering syntaktisk mere tilgængelig, især gennem hans Boost.Hana-bibliotek. Dionne sammen med Andrew Sutton, Herb Sutter, David Vandevoorde og andre er nu i spidsen for bestræbelserne i standardiseringsudvalget for at give metaprogrammering førsteklasses støtte på sproget. Et vigtigt grundlag for dette arbejde er udforskningen af, hvilke programegenskaber der skal være tilgængelige gennem refleksion; Matúš Chochlík, Axel Naumann og David Sankel er hovedbidragere i dette område.

John J. Barton og Lee R. Nackman illustrerede, hvordan man holder styr på dimensionelle enheder, når man udfører beregninger. SIunits-biblioteket var et mere omfattende bibliotek til håndtering af fysiske enheder udviklet af Walter Brown. Det std :: chrono komponent i standardbiblioteket beskæftiger sig kun med tid og datoer og blev bidraget af Howard Hinnant.