Definisjonen av en mal for en bestemt type parameter kalles. Malfunksjoner

17. mars 2009 kl. 20:36

C ++ Mal Spesialiseringstriks

  • C++

Malspesialisering er en av de "komplekse" funksjonene til C++-språket og brukes hovedsakelig når du oppretter biblioteker. Dessverre er noen av detaljene ved malspesialisering ikke godt dekket i populære bøker om dette språket. Dessuten beskriver selv de 53 sidene av den offisielle ISO-språkstandarden som er viet til maler interessante detaljer i en rotete, og lar mye å "gjette deg selv - det er åpenbart." Under kuttet prøvde jeg å tydelig angi de grunnleggende prinsippene for malspesialisering og vise hvordan disse prinsippene kan brukes i konstruksjonen av magiske trollformler.

Hei Verden

Hvordan er vi vant til å bruke maler? Bruk malnøkkelordet etterfulgt av navnene i vinkelparenteser mal parametere etterfulgt av type og navn. For parametere indikerer de også hva det er: en type (typenavn) eller en verdi (for eksempel int). Selve maltypen kan være en klasse (klasse), struktur (struct er faktisk en klasse) eller funksjon (bool foo () og så videre). For eksempel kan den enkleste malte klassen "A" defineres slik:

Etter en stund vil vi at klassen vår skal fungere likt for alle typer, men forskjellig for noe vanskelig som int. Bullshit spørsmål, skrive en spesialisering: det ser det samme ut som en annonse men parametere vi angir ikke malen i vinkelparenteser, i stedet angir vi spesifikke argumenter mal etter navnet:

Mal<>klasse A< int >(); // int her er malargumentet
Ferdig, kan du skrive metoder og felt for en spesiell implementering for int. Denne spesialiseringen kalles vanligvis fullstendig(full spesialisering eller eksplisitt spesialisering). For de fleste praktiske oppgaver er det ikke nødvendig med mer. Og om nødvendig, så...

Egendefinert mal er en ny mal

Hvis du leser ISO C ++-standarden nøye, kan du finne en interessant uttalelse: ved å lage en spesialisert malklasse, lager vi ny malt klasse(14.5.4.3). Hva gir det oss? En spesialisert malklasse kan inneholde metoder, felt eller typedeklarasjoner som ikke er i malklassen vi spesialiserer. Det er praktisk når du trenger en malklassemetode for å fungere bare for en spesifikk spesialisering - det er nok å erklære metoden bare i denne spesialiseringen, kompilatoren vil gjøre resten:

En spesialisert mal kan ha sine egne malparametere

Djevelen, som du vet, er i detaljene. At en spesialisert malklasse er en helt ny og egen klasse er absolutt interessant, men det er ikke mye magi i dette. Og magien er i en ubetydelig konsekvens - hvis det er en egen malklasse, kan den ha separate, ikke på noen måte forbundet med en ikke-spesialisert malklasse. parametere(parametere er det som står etter mal i vinkelparenteser). For eksempel slik:

Mal< typename S, typename U >klasse A< int > {};
Riktignok er dette nøyaktig koden kompilatoren vil ikke kompilere- vi bruker ikke de nye malparametrene S og U, som er forbudt for en spesialisert malklasse (men den spesialiserte kompilatoren forstår at dette er en klasse fordi den har samme navn "A" som den allerede erklærte malklassen). Kompilatoren vil til og med si en spesiell feil: "eksplisitt spesialisering bruker delvis spesialiseringssyntaks, bruk mal<>i stedet ". Det antyder at hvis det ikke er noe å si, så må du bruke mal<>og ikke vise seg frem. Hvorfor kan så nye parametere brukes i en spesialisert malklasse? Svaret er merkelig - for å spørre argumenter spesialiseringer (argumenter er det som står etter klassenavnet i vinkelparentes). Det vil si at ved å spesialisere en malklasse, kan vi, i stedet for en enkel og forståelig int, spesialisere den gjennom nye parametere:

Mal< typename S, typename U >klasse A< std::map< S, U > > {};
En slik merkelig notasjon vil kompilere. Og når du bruker den resulterende malklassen med std :: map, vil en spesialisering bli brukt, der std :: kartnøkkeltypen vil være tilgjengelig som en parameter for den nye malen S, og std :: kartverditypen som U.

En slik spesialisering av malen, der en ny liste med parametere settes og gjennom disse parameterne settes argumenter for spesialiseringen, kalles delvis spesialisering(delspesialisering). Hvorfor "delvis"? Angivelig fordi det opprinnelig ble tenkt som en syntaks for å spesialisere en mal, ikke for alle argumenter. Et eksempel hvor en malklasse med to parametere spesialiserer seg på bare én av dem (spesialisering vil fungere når det første argumentet, T, er spesifisert som int. I dette tilfellet kan det andre argumentet være hva som helst - for dette er en ny parameter U introdusert i delspesialiseringen og er spesifisert i listen argumenter for spesialisering):

Mal< typename T, typename S >klasse B (); mal< typename U >klasse B< int, U > {};

De magiske konsekvensene av delvis spesialisering

Det er en rekke interessante implikasjoner av de to egenskapene til malspesialisering beskrevet ovenfor. For eksempel, når du bruker delvis spesialisering, kan du, ved å introdusere nye malparametere og beskrive spesialiserte argumenter gjennom dem, bryte sammensatte typer i de enkleste. I eksemplet nedenfor vil den spesialiserte malklassen A bli brukt hvis malargumentene er av typen funksjonspeker. Samtidig, gjennom de nye parameterne til malen S og U, kan du få typen returverdi til denne funksjonen og typen argument:

Mal< typename S, typename U >klasse A< S(*)(U) > {};
Og hvis du erklærer typedef eller static const int i en spesialisert mal (ved å utnytte det faktum at dette er en ny mal), kan du bruke den til å trekke ut nødvendig informasjon fra typen. For eksempel bruker vi en malt klasse for å lagre objekter og ønsker å få størrelsen på det beståtte objektet, eller 0 hvis det er en peker. På to linjer:

Mal< typename T >struct Get (const static int Størrelse = sizeof (T); mal< typename S >struct Get< S* >(konst statisk int Størrelse = 0;); Få< int >:: Størrelse // f.eks. 4 Få< int* >:: Størrelse // 0 - fant pekeren :)
Denne typen magi brukes hovedsakelig i biblioteker: stl, boost, loki, og så videre. Selvfølgelig, i programmering på høyt nivå, er det tungvint å bruke slike triks - jeg tror alle husker konstruksjonen for å få størrelsen på en matrise :). Men i biblioteker gjør delvis spesialisering det relativt enkelt å implementere delegater, arrangementer, komplekse beholdere og andre til tider svært nødvendige og nyttige ting.

Kolleger, hvis du finner en feil (og jeg, dessverre, er ikke en guru - jeg kan ta feil) eller du har kritikk, spørsmål eller tillegg til ovenstående, vil jeg gjerne kommentere.

Oppdatering: Lovet oppfølger

En malfunksjon definerer et generelt sett med operasjoner som vil bli brukt på forskjellige typer data. Ved å bruke denne mekanismen kan noen generelle algoritmer brukes på et bredt spekter av data. Som du vet, er mange algoritmer logisk sett de samme uavhengig av hvilken type data de opererer med. For eksempel er Quicksort-algoritmen den samme for en matrise med heltall og for en matrise med flyttall. Bare typen data som skal sorteres er forskjellig. Ved å lage en generisk funksjon kan du definere essensen av en algoritme uten hensyn til datatype. Kompilatoren genererer deretter automatisk den riktige koden for datatypen som denne spesielle funksjonsimplementeringen er opprettet for på kompileringstidspunktet. I hovedsak, når en malfunksjon opprettes, opprettes en funksjon som automatisk kan overbelaste seg selv.

Malfunksjoner opprettes ved å bruke malnøkkelordet. Den vanlige betydningen av ordet "mal" gjenspeiler ganske fullt bruken i C ++. Malen brukes til å lage skjelettet til funksjonen, og overlater implementeringsdetaljene til kompilatoren. Den generelle formen for en malfunksjon er som følger:

mal returtype funksjonsnavn (parameterliste)
{
// funksjonskropp
}

Her er ptyp en typeparameter, en plassholder for datatypenavnet som brukes av funksjonen. Denne typeparameteren kan brukes i en funksjonsdefinisjon. Dette er imidlertid kun en "plassholder" som automatisk erstattes av kompilatoren med den faktiske datatypen når du oppretter en spesifikk versjon av funksjonen.

Nedenfor er et kort eksempel som lager en malfunksjon som har to parametere. Denne funksjonen endrer verdiene til verdiene til disse parameterne seg imellom. Siden den generelle prosessen med å utveksle verdier mellom to variabler ikke avhenger av deres type, kan den implementeres naturlig ved hjelp av en malfunksjon.

// eksempel på en funksjonsmal
#inkludere
// funksjonsmal
mal ugyldig bytte (X & a, X & b)
{
X temp;
temp = a;
a = b;
b = temp;
}
int main ()
{
int i = 10, j = 20;
float x = 10,1, y = 23,3;
char a = "x", b = "z";
cout<< "Original i, j: " << i << " " << j << endl;
cout<< "Original x, y: " << x << " " << у << endl;
cout<< "Original a, b: " << a << " " << b << endl;
bytte (i, j); // bytt heltall
bytte (x, y); // utveksling av reelle verdier
bytte (a, b); // tegnbytte
cout<< "Swapped i, j: " << i << " " << j << endl;
cout<< "Swapped x, y: " << x << " " << у << endl;
cout<< "Swapped a, b: " << a << " " << b << endl;
returner 0;
}

La oss se nærmere på dette programmet. Linje

Mal ugyldig bytte (X & a, X & b)

Indikerer for kompilatoren at en mal blir generert. Her er X en typemal som brukes som en typeparameter. Følgende er erklæringen av swap ()-funksjonen som bruker datatypen X for de parameterne som vil utveksle verdier. I hovedfunksjonen () kalles swap-funksjonen () å sende tre forskjellige typer data til den: heltall, flyttall og tegn. Siden swap ()-funksjonen er en malfunksjon, vil kompilatoren automatisk lage tre forskjellige versjoner av swap ()-funksjonen - en for å arbeide med heltall, en for å arbeide med flyttall og til slutt en for å arbeide med variabler av et tegn type.

Leksjon 29. Bruke funksjonsmaler

Når du oppretter funksjoner, oppstår det noen ganger situasjoner når to funksjoner utfører samme behandling, men opererer på forskjellige datatyper (for eksempel bruker den ene parametere av typen int, og den andre av typen float). Som du allerede vet fra leksjon 13, ved å brukeen, kan du bruke samme navn for funksjoner som utfører forskjellige handlinger og har forskjellige typer parametere. Men hvis funksjoner returnerer verdier av forskjellige typer, bør du bruke unike navn for dem (se merknaden for leksjon 13). Anta for eksempel at du har en funksjon kalt maks som returnerer maksimalt to heltallsverdier. Hvis du senere trenger en lignende funksjon som returnerer maksimalt to flyttallsverdier, bør du definere en annen funksjon, for eksempel fmax. I denne opplæringen lærer du hvordan du bruker C++-maler for raskt å lage funksjoner som returnerer forskjellige typer verdier. Ved slutten av denne leksjonen vil du ha mestret følgende grunnleggende konsepter:

    En mal definerer et sett med utsagn som programmene dine senere kan lage flere funksjoner med.

    Programmer bruker ofte funksjonsmaler for raskt å definere flere funksjoner som, ved å bruke de samme operatorene, opererer på forskjellige parametertyper eller har forskjellige returtyper.

    Funksjonsmaler har spesifikke navn som tilsvarer funksjonsnavnet du bruker i programmet.

    Når programmet ditt har definert en funksjonsmal, kan det opprette en spesifikk funksjon ved å bruke den malen for å definere en prototype som inkluderer navnet på malen, funksjonens returverdi og parametertyper.

    Under kompilering vil C++-kompilatoren lage funksjoner i programmet ditt ved å bruke typene spesifisert i funksjonsprototypene som refererer til malnavnet.

Funksjonsmaler har unik syntaks som kan være forvirrende ved første øyekast. Etter å ha laget en eller to maler, vil du imidlertid oppdage at de faktisk er veldig enkle å bruke.

LAG ET ENKELT FUNKSJONSMØNSTER

En funksjonsmal definerer en typeuavhengig funksjon. Ved å bruke en slik mal kan programmene dine senere definere spesifikke funksjoner med de nødvendige typene. Følgende er for eksempel en mal for en funksjon kalt max som returnerer den største av to verdier:

mal T maks (T a, T b)

(hvis (a> b) returner (a); ellers returnerer (b);)

Bokstaven T i dette tilfellet representerer en generisk maltype. Etter å ha definert en mal i programmet ditt, erklærer du funksjonsprototyper for hver type du trenger. Når det gjelder maks-malen, lager følgende prototyper funksjoner av typen float og int.

flyte maks (flyte, flyte); int maks (int, int);

Når C ++ kompilatoren møter disse prototypene, vil den erstatte maltypen T med typen du spesifiserer når du konstruerer funksjonen. Når det gjelder flytetypen, vil maksfunksjonen etter utskifting ha følgende form:

mal T maks (T a, T b)

(hvis (a> b) returner (a); ellers returnerer (b);)

float max (float a, float b)

(hvis (a> b) returner (a); ellers returnerer (b);)

Følgende MAX_TEMR.CPP-program bruker maks-malen til å lage en funksjon av typen int og float.

#inkludere

mal T maks (T a, T b)

(hvis (a> b) returner (a); ellers returnerer (b);)

flyte maks (flyte, flyte);

int maks (int, int);

(cout<< "Максимум 100 и 200 равен " << max(100, 200) << endl; cout << "Максимум 5.4321 и 1.2345 равен " << max(5.4321, 1.2345) << endl; }

Under kompilering genererer C++-kompilatoren automatisk setninger for å konstruere en funksjon som opererer på int-typen og en andre funksjon som opererer på float-typen. Fordi C ++-kompilatoren administrerer setninger som tilsvarer funksjoner du oppretter ved hjelp av maler, lar den deg bruke de samme navnene for funksjoner som returnerer forskjellige typer verdier. Du kunne ikke gjøre dette med bare funksjonsoverbelastning, som diskutert i leksjon 13.

Bruke funksjonsmaler

Etter hvert som programmene dine blir mer komplekse, kan det oppstå situasjoner der du trenger lignende funksjoner som utfører de samme operasjonene, men på forskjellige datatyper. En funksjonsmal lar programmene dine definere en generisk eller typeuavhengig funksjon. Når et program trenger å bruke en funksjon for en bestemt type, for eksempel int eller float, spesifiserer det en funksjonsprototype som bruker funksjonsmalnavnet og retur- og parametertypene. Under kompilering vil C ++ lage den tilsvarende funksjonen. Ved å lage maler reduserer du antall funksjoner du må kode selv, og programmene dine kan bruke samme navn på funksjoner som utfører en bestemt operasjon, uavhengig av funksjonens returverdi og parametertyper.

MALER SOM BRUKER FLERE TYPER

Den forrige maldefinisjonen for maks-funksjonen brukte en enkelt generisk type, T. Svært ofte kreves det flere typer i en funksjonsmal. Følgende utsagn lager for eksempel en mal for funksjonen show_array, som viser elementene i en matrise. Malen bruker type T for å definere typen av matrisen, og type T1 for å indikere typen telleparameter:

mal

< count; index++) cout << array << " "; cout << endl; }

Som før må programmet spesifisere funksjonsprototyper for de nødvendige typene:

void show_array (int *, int); void show_array (float *, usignert);

Følgende program SHOW_TEM.CPP bruker en mal for å lage funksjoner som sender ut matriser av typen int og type float.

#inkludere

mal void show_array (T * array, T1 count)

(T1-indeks; for (indeks = 0; indeks< count; index++) cout << array “ " "; cout << endl; }

void show_array (int *, int);

void show_array (float *, usignert);

(int pages = (100, 200, 300, 400, 500); flytende priserH = (10.05, 20.10, 30.15); show_array (sider, 5); show_array (priser, 3);)

Maler og flere typer

Ettersom funksjonsmaler blir mer komplekse, kan de gi støtte for flere typer. Programmet ditt kan for eksempel lage en mal for en funksjon kalt array_sort som sorterer elementene i en matrise. I dette tilfellet kan funksjonen bruke to parametere: den første, som tilsvarer matrisen, og den andre, som tilsvarer antall elementer i matrisen. Hvis programmet antar at arrayet aldri vil inneholde mer enn 32767 verdier, kan det bruke int-typen for array size-parameteren. Et mer generisk mønster kan imidlertid gi programmet muligheten til å spesifisere sin egen type for denne parameteren, som vist nedenfor:

mal T , klasse T1> void array_sort (T-array, T1-elementer)

(// operatører)

Ved å bruke malen array_sort kan et program lage funksjoner som sorterer små flyter (mindre enn 128 elementer) og veldig store int-matriser ved å bruke følgende prototyper:

void array_sort (float, char); void array_sort (int, long);

HVA DU TRENGER Å VITE

Som du allerede vet, reduserer bruk av funksjonsmaler programmering ved å la C ++-kompilatoren generere setninger for funksjoner som bare er forskjellige i retur- og parametertyper. I leksjon 30 lærer du hvordan du bruker maler for å lage typeuavhengige eller generiske klasser. Før du fullfører leksjon 30, sørg for at du mestrer følgende grunnleggende konsepter:

      Funksjonsmaler lar deg deklarere typeuavhengige, eller generiske, funksjoner.

      Når programmet ditt trenger å bruke en funksjon med visse datatyper, må det gi en funksjonsprototype som definerer de nødvendige typene.

      Når C ++ kompilatoren møter en slik funksjonsprototype, vil den generere setninger som tilsvarer den funksjonen, og erstatte de nødvendige typene.

      Programmene dine bør lage maler for vanlige funksjoner som fungerer med forskjellige typer. Med andre ord, hvis du bare bruker én type med en funksjon, er det ikke nødvendig å bruke en mal.

      Hvis en funksjon krever flere typer, tildeler malen ganske enkelt hver type en unik identifikator, for eksempel T, T1 og T2. Senere i kompileringsprosessen vil C++-kompilatoren riktig tilordne typene du spesifiserte i funksjonsprototypen.

Malspesialisering er en av de "komplekse" funksjonene til C++-språket og brukes hovedsakelig når du oppretter biblioteker. Dessverre er noen av detaljene ved malspesialisering ikke godt dekket i populære bøker om dette språket. Dessuten beskriver selv de 53 sidene av den offisielle ISO-språkstandarden som er viet til maler interessante detaljer i en rotete, og lar mye å "gjette deg selv - det er åpenbart." Under kuttet prøvde jeg å tydelig angi de grunnleggende prinsippene for malspesialisering og vise hvordan disse prinsippene kan brukes i konstruksjonen av magiske trollformler.

Hei Verden

Hvordan er vi vant til å bruke maler? Bruk malnøkkelordet etterfulgt av navnene i vinkelparenteser mal parametere etterfulgt av type og navn. For parametere indikerer de også hva det er: en type (typenavn) eller en verdi (for eksempel int). Selve maltypen kan være en klasse (klasse), struktur (struct er faktisk en klasse) eller funksjon (bool foo () og så videre). For eksempel kan den enkleste malte klassen "A" defineres slik:

Etter en stund vil vi at klassen vår skal fungere likt for alle typer, men forskjellig for noe vanskelig som int. Bullshit spørsmål, skrive en spesialisering: det ser det samme ut som en annonse men parametere vi angir ikke malen i vinkelparenteser, i stedet angir vi spesifikke argumenter mal etter navnet:

Mal<>klasse A< int >(); // int her er malargumentet
Ferdig, kan du skrive metoder og felt for en spesiell implementering for int. Denne spesialiseringen kalles vanligvis fullstendig(full spesialisering eller eksplisitt spesialisering). For de fleste praktiske oppgaver er det ikke nødvendig med mer. Og om nødvendig, så...

Egendefinert mal er en ny mal

Hvis du leser ISO C ++-standarden nøye, kan du finne en interessant uttalelse: ved å lage en spesialisert malklasse, lager vi ny malt klasse(14.5.4.3). Hva gir det oss? En spesialisert malklasse kan inneholde metoder, felt eller typedeklarasjoner som ikke er i malklassen vi spesialiserer. Det er praktisk når du trenger en malklassemetode for å fungere bare for en spesifikk spesialisering - det er nok å erklære metoden bare i denne spesialiseringen, kompilatoren vil gjøre resten:

En spesialisert mal kan ha sine egne malparametere

Djevelen, som du vet, er i detaljene. At en spesialisert malklasse er en helt ny og egen klasse er absolutt interessant, men det er ikke mye magi i dette. Og magien er i en ubetydelig konsekvens - hvis det er en egen malklasse, kan den ha separate, ikke på noen måte forbundet med en ikke-spesialisert malklasse. parametere(parametere er det som står etter mal i vinkelparenteser). For eksempel slik:

Mal< typename S, typename U >klasse A< int > {};
Riktignok er dette nøyaktig koden kompilatoren vil ikke kompilere- vi bruker ikke de nye malparametrene S og U, som er forbudt for en spesialisert malklasse (men den spesialiserte kompilatoren forstår at dette er en klasse fordi den har samme navn "A" som den allerede erklærte malklassen). Kompilatoren vil til og med si en spesiell feil: "eksplisitt spesialisering bruker delvis spesialiseringssyntaks, bruk mal<>i stedet ". Det antyder at hvis det ikke er noe å si, så må du bruke mal<>og ikke vise seg frem. Hvorfor kan så nye parametere brukes i en spesialisert malklasse? Svaret er merkelig - for å spørre argumenter spesialiseringer (argumenter er det som står etter klassenavnet i vinkelparentes). Det vil si at ved å spesialisere en malklasse, kan vi, i stedet for en enkel og forståelig int, spesialisere den gjennom nye parametere:

Mal< typename S, typename U >klasse A< std::map< S, U > > {};
En slik merkelig notasjon vil kompilere. Og når du bruker den resulterende malklassen med std :: map, vil en spesialisering bli brukt, der std :: kartnøkkeltypen vil være tilgjengelig som en parameter for den nye malen S, og std :: kartverditypen som U.

En slik spesialisering av malen, der en ny liste med parametere settes og gjennom disse parameterne settes argumenter for spesialiseringen, kalles delvis spesialisering(delspesialisering). Hvorfor "delvis"? Angivelig fordi det opprinnelig ble tenkt som en syntaks for å spesialisere en mal, ikke for alle argumenter. Et eksempel hvor en malklasse med to parametere spesialiserer seg på bare én av dem (spesialisering vil fungere når det første argumentet, T, er spesifisert som int. I dette tilfellet kan det andre argumentet være hva som helst - for dette er en ny parameter U introdusert i delspesialiseringen og er spesifisert i listen argumenter for spesialisering):

Mal< typename T, typename S >klasse B (); mal< typename U >klasse B< int, U > {};

De magiske konsekvensene av delvis spesialisering

Det er en rekke interessante implikasjoner av de to egenskapene til malspesialisering beskrevet ovenfor. For eksempel, når du bruker delvis spesialisering, kan du, ved å introdusere nye malparametere og beskrive spesialiserte argumenter gjennom dem, bryte sammensatte typer i de enkleste. I eksemplet nedenfor vil den spesialiserte malklassen A bli brukt hvis malargumentene er av typen funksjonspeker. Samtidig, gjennom de nye parameterne til malen S og U, kan du få typen returverdi til denne funksjonen og typen argument:

Mal< typename S, typename U >klasse A< S(*)(U) > {};
Og hvis du erklærer typedef eller static const int i en spesialisert mal (ved å utnytte det faktum at dette er en ny mal), kan du bruke den til å trekke ut nødvendig informasjon fra typen. For eksempel bruker vi en malt klasse for å lagre objekter og ønsker å få størrelsen på det beståtte objektet, eller 0 hvis det er en peker. På to linjer:

Mal< typename T >struct Get (const static int Størrelse = sizeof (T); mal< typename S >struct Get< S* >(konst statisk int Størrelse = 0;); Få< int >:: Størrelse // f.eks. 4 Få< int* >:: Størrelse // 0 - fant pekeren :)
Denne typen magi brukes hovedsakelig i biblioteker: stl, boost, loki, og så videre. Selvfølgelig, i programmering på høyt nivå, er det tungvint å bruke slike triks - jeg tror alle husker konstruksjonen for å få størrelsen på en matrise :). Men i biblioteker gjør delvis spesialisering det relativt enkelt å implementere delegater, arrangementer, komplekse beholdere og andre til tider svært nødvendige og nyttige ting.

Kolleger, hvis du finner en feil (og jeg, dessverre, er ikke en guru - jeg kan ta feil) eller du har kritikk, spørsmål eller tillegg til ovenstående, vil jeg gjerne kommentere.

Oppdatering: Lovet oppfølger