Programmering

Introduksjon til metaprogrammering i C ++

Forrige 1 2 3 Side 3 Side 3 av 3
  • Statusvariabler: Malparametrene
  • Loop konstruksjoner: Gjennom rekursjon
  • Kjøring stier valg: Ved å bruke betingede uttrykk eller spesialiseringer
  • Heltalsregning

Hvis det ikke er noen grenser for mengden rekursive instantiasjoner og antall tilstandsvariabler som er tillatt, er dette tilstrekkelig til å beregne alt som kan beregnes. Det kan imidlertid ikke være praktisk å bruke maler. Videre, fordi malinstansiering krever betydelige kompileringsressurser, reduserer omfattende rekursiv instantiering raskt en kompilator eller til og med utnytter de tilgjengelige ressursene. C ++ -standarden anbefaler, men krever ikke, at 1024 nivåer av rekursive instantiations skal tillates som et minimum, noe som er tilstrekkelig for de fleste (men absolutt ikke alle) malmetaprogrammeringsoppgaver.

I praksis bør malmetaprogrammer brukes sparsomt. Det er imidlertid noen få situasjoner når de er uerstattelige som et verktøy for å implementere praktiske maler. Spesielt kan de noen ganger være skjult i indre av mer konvensjonelle maler for å presse mer ytelse ut av kritiske algoritmeimplementeringer.

Rekursive instantiering kontra rekursive malargumenter

Vurder følgende rekursive mal:

malstruktur Doublify {}; mal struktur Trouble {using LongType = Doublify; }; mal struktur Trouble {using LongType = double; }; Trouble :: LongType ouch;

Bruken av Problem :: LongType ikke bare utløser rekursiv instantiering av Problemer, Problemer, …, Problemer, men det instantierer også Dobler over stadig mer komplekse typer. Tabellen illustrerer hvor raskt den vokser.

Veksten av Problem :: LongType

 
Skriv inn aliasUnderliggende type
Problem :: LongTypedobbelt
Problem :: LongTypeDobler
Problem :: LongTypeDobler<>

Dobler>

Problem :: LongTypeDobler<>

Dobler>,

   <>

Dobler >>

Som tabellen viser, er kompleksiteten i typebeskrivelsen av uttrykket Problem :: LongType vokser eksponentielt med N. Generelt understreker en slik situasjon en C ++ - kompilator enda mer enn rekursive instantiations som ikke involverer rekursive malargumenter. Et av problemene her er at en kompilator holder en representasjon av det manglede navnet for typen. Dette manglede navnet koder den eksakte malspesialiseringen på en eller annen måte, og tidlige C ++ implementeringer brukte en koding som er omtrent proporsjonal med lengden på mal-id. Disse kompilatorene brukte deretter godt over 10.000 tegn til Problem :: LongType.

Nyere C ++ -implementasjoner tar hensyn til det faktum at nestede mal-ID-er er ganske vanlige i moderne C ++ -programmer og bruker smarte komprimeringsteknikker for å redusere veksten i navnekoding betydelig (for eksempel noen hundre tegn for Problem :: LongType). Disse nyere kompilatorene unngår også å generere et manglet navn hvis det faktisk ikke er behov for noe fordi det ikke genereres noe lavt nivåkode for malforekomsten. Til tross for at alt annet er likt, er det sannsynligvis å foretrekke å organisere rekursiv instantiering på en slik måte at malargumenter ikke også trenger å bli nestet rekursivt.

Oppregningsverdier kontra statiske konstanter

I de tidlige dagene av C ++ var opptellingsverdier den eneste mekanismen for å skape "sanne konstanter" (kalt konstant-uttrykk) som navngitte medlemmer i klassedeklarasjoner. Med dem kan du for eksempel definere en Pow3 metaprogram for å beregne krefter på 3 som følger:

meta / pow3enum.hpp // primærmal for å beregne 3 til Nth mal struct Pow3 {enum {verdi = 3 * Pow3 :: verdi}; }; // full spesialisering for å avslutte rekursjonsmalen struct Pow3 {enum {value = 1}; };

Standardiseringen av C ++ 98 introduserte konseptet med klassiske statiske konstantinitialiserere, slik at Pow3-metaprogrammet kunne se ut som følger:

meta / pow3const.hpp // primærmal for å beregne 3 til Nth template struct Pow3 {statisk int const verdi = 3 * Pow3 :: verdi; }; // full spesialisering for å avslutte rekursjonsmalen struct Pow3 {statisk int const verdi = 1; };

Imidlertid er det en ulempe med denne versjonen: Statiske konstante medlemmer er verdier. Så hvis du har en erklæring som

ugyldig foo (int const &);

og du gir det resultatet av et metaprogram:

foo (Pow3 :: verdi);

en kompilator må bestå adresse av Pow3 :: verdi, og som tvinger kompilatoren til å sette i gang og tildele definisjonen for det statiske medlemmet. Som et resultat er beregningen ikke lenger begrenset til en ren “kompileringstid” -effekt.

Opptellingsverdier er ikke verdier (det vil si at de ikke har en adresse). Så når du sender dem som referanse, brukes ikke noe statisk minne. Det er nesten akkurat som om du passerte den beregnede verdien som en bokstav.

C ++ 11 ble imidlertid introdusert constexpr medlemmer av statiske data, og de er ikke begrenset til integrerte typer. De løser ikke adressespørsmålet som er nevnt ovenfor, men til tross for den mangelen er de nå en vanlig måte å produsere resultater av metaprogrammer på. De har fordelen av å ha en riktig type (i motsetning til en kunstig enumtype), og den typen kan trekkes ut når det statiske medlemmet blir erklært med den automatiske typespesifikatoren. C ++ 17 la til innebygde statiske data-medlemmer, som løser adresseproblemet som er reist ovenfor, og kan brukes med constexpr.

Metaprogrammeringshistorie

Det tidligste dokumenterte eksemplet på et metaprogram var av Erwin Unruh, som deretter representerte Siemens i C ++ standardiseringskomiteen. Han bemerket beregningsfullhet av malinstansieringsprosessen og demonstrerte poenget sitt ved å utvikle det første metaprogrammet. Han brukte Metaware-kompilatoren og lokket den til å utstede feilmeldinger som ville inneholde suksessive primtall. Her er koden som ble sirkulert på et C ++ komiteemøte i 1994 (modifisert slik at den nå kompilerer på standard samsvarende kompilatorer):

meta / unruh.cpp // beregning av primtall // (modifisert med tillatelse fra original fra 1994 av Erwin Unruh) mal struct is_prime {enum ((p% i) && is_prime2? p: 0), i-1> :: pri); }; malstrukturen er_prime {enum {pri = 1}; }; malstrukturen er_prime {enum {pri = 1}; }; mal struktur D {D (ugyldig *); }; mal struct CondNull {static int const value = i; }; mal struktur CondNull {statisk ugyldig * verdi; }; ugyldig * CondNull :: verdi = 0; mal struct Prime_print {

// primærmal for sløyfe for å skrive ut primtall Prime_print a; enum {pri = is_prime :: pri}; ugyldig f () {D d = CondNull :: verdi;

// 1 er en feil, 0 er bra a.f (); }}; mal struktur Prime_print {

// full spesialisering for å avslutte loop enum {pri = 0}; ugyldig f () {D d = 0; }; }; #ifndef SISTE #definer SISTE 18 #endif int main () {Prime_print a; a.f (); }

Hvis du kompilerer dette programmet, vil kompilatoren skrive ut feilmeldinger når, i Prime_print :: f ()initialiseringen av d mislykkes. Dette skjer når den opprinnelige verdien er 1 fordi det bare er en konstruktør for ugyldig *, og bare 0 har en gyldig konvertering til tomrom*. For eksempel får vi (blant flere andre meldinger) følgende feil på en kompilator:

unruh.cpp: 39: 14: feil: ingen levedyktig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: error: ingen levedyktig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: feil: ingen levedyktig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: feil: ingen levedyktig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: feil: ingen levedyktig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: feil: ingen levedyktig konvertering fra 'const int' til 'D' unruh.cpp: 39: 14: feil: ingen levedyktig konvertering fra 'const int' til 'D'

Merk: Siden feilhåndtering i kompilatorer er forskjellig, kan det hende at noen kompilatorer stopper etter at den første feilmeldingen er skrevet ut.

Konseptet med C ++ malmetaprogrammering som et seriøst programmeringsverktøy ble først populært (og noe formalisert) av Todd Veldhuizen i sin artikkel "Using C ++ Template Metaprograms." Veldhuizens arbeid med Blitz ++ (et numerisk array-bibliotek for C ++) introduserte også mange forbedringer og utvidelser av metaprogrammering (og teknikker for uttrykksmal).

Både den første utgaven av denne boka og Andrei Alexandrescu’s Moderne C ++ design bidro til en eksplosjon av C ++ - biblioteker som utnyttet malbasert metaprogrammering ved å katalogisere noen av de grunnleggende teknikkene som fremdeles er i bruk i dag. Boost-prosjektet var medvirkende til å bringe orden på denne eksplosjonen. Tidlig introduserte den MPL (metaprogramming library), som definerte et konsistent rammeverk for skriv metaprogrammering gjort populær også gjennom David Abrahams og Aleksey Gurtovoys bok C ++ malmetaprogrammering.

Ytterligere viktige fremskritt har blitt gjort av Louis Dionne for å gjøre metaprogrammering syntaktisk mer tilgjengelig, spesielt gjennom hans Boost.Hana-bibliotek. Dionne, sammen med Andrew Sutton, Herb Sutter, David Vandevoorde og andre, leder nå innsatsen i standardiseringskomiteen for å gi metaprogrammering førsteklasses støtte på språket. Et viktig grunnlag for det arbeidet er utforskningen av hvilke programegenskaper som skal være tilgjengelige gjennom refleksjon; Matúš Chochlík, Axel Naumann og David Sankel er viktigste bidragsytere i dette området.

John J. Barton og Lee R. Nackman illustrerte hvordan man kan holde oversikt over dimensjonsenheter når man utfører beregninger. SIunits-biblioteket var et mer omfattende bibliotek for å håndtere fysiske enheter utviklet av Walter Brown. De std :: chrono komponenten i standardbiblioteket omhandler bare tid og datoer, og ble bidratt av Howard Hinnant.

$config[zx-auto] not found$config[zx-overlay] not found