9.
Templates
· Vad är templates?
· Funktionsmallar.
· Klassmallar.
· Användningsområden.
· Ett exempel.
Vad är templates?
Med hjälp av templates kan man definiera en grupp funktioner eller klasser som kan operera på olika typer av information. Det handlar om att man vill göra en en enda funktion eller en klass som fungerar med olika datatyper i stället för att deklarera en funktion/klass för varje datatyp som den ska fungera med. Templates är en mekanism som möjliggör att man deklarerar typparametrar för detta ändamål. På svenska kallar man ofta dessa templates för ‘mallar’, eftersom en template är en typoberoende mall för en funktion eller en klass. Man talar om ‘parameterized types’ parameterstyrda datatyper.
Om man t.ex. vill skapa en funktion som returnerar det minsta av två angivna värden skulle man på vedertaget C++ sätt skriva ett antal överlagrade funktioner som kan arbeta på olika datatyper, en för heltal, en för flyttal etc:
// min för heltal
int min( int a, int b )
return ( a < b ) ? a : b;
// min för långt heltal
long min( long
a, long b )
return ( a < b ) ? a : b;
// min för tecken
char min( char a, char b )
return ( a < b ) ? a : b;
//etc...
Använder man templates kan man reducera detta till att deklarera en enda funktion som fungerar på alla datatyper:
template
<class T> T min( T a, T b )
return ( a < b ) ? a : b;
Således kan man inte bara spara skrivarbete utan även göra sina funktioner och klasser typsäkra, d.v.s. se till så att de fungerar på valfria typer.
Syntax:
template <argumentlista> funktionsdeklaration
Argumentlistan räknar upp de argument som används av mallfunktionen (template function), och anger vilken del av den efterföljande koden som kan variera med avseende på typ. Ett exempel:
template <class T, int i> class CMinKlass
I det här fallet har vi en mall som kan ta emot en valfri typ (class T) och en konstant (int i). Mallen kommer att använda datatypen T och konstanten i när den konstruerar ett objekt baserat på klassen CMinKlass. Klassen CMinKlass måste referera till ‘T’.
Själva deklarationen av en mall (template) genererar ingen kod, den deklarerar en grupp klasser eller funktioner, vilka kommer att skapas när man första gången använder mallen i programmet, och då är det en speciell version, av funktionen eller klassen baserad på angivna datatyper, som skapas. Denna version återanvänds om man använder mallen fler gånger med samma datatyper, så den kommer bara att finnas i ett exemplar i exe-filen.
De vanliga typkonverteringarna fungerar inte på mallar. Kompilatorn kommer i första hand att söka en funktion/klass som stämmer exakt överens med de argument som används i källkoden. I andra hand försöker den använda den deklare-rade mallen till att skapa en funktion/klass som stämmer exakt överens med de argument som används i källkoden. I sista hand försöker den att tillämpa reglerna för överlagring, och om det inte går så skrivs ett felmeddelande.
Alla malldeklarationer har globalt sammanhang (global scope).
Funktionsmallar.
Vi kan testa det här med funktionsmallar genom att damma av det gamla exemplet ‘byt’ som gav oss så mycket skrivarbete förut:
void Byt(int
*a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
Vi byter nu ut funktionen mot en funktionsmall:
template
<class T> void Byt( T& a, T& b )
{
T temp( a );
a = b; b = temp;
}
Här har vi en hel ‘familj’ med funktioner på en gång, vilka byter variabler av valfri typ. Observera att detta även fungerar med egendefinierade typer. Skulle man använda den till att byta innehållet i två objekt måste man dock tänka på att eventuellt behov av överlagrade operatorer och kopieringskonstruktor är korrekt deklarerade i den klass objekten baseras på.
Dessutom ser mallhanteringen till att programmeraren inte försöker byta två variabler eller objekt av olika typ. Kompilatorn ser felet eftersom den känner till de olika typerna vid kompileringstillfället.
Lägg märke till att alla de parametrar som anges för mallen, d.v.s. de som står angivna mellan ‘<‘ och ‘>‘, måste användas i den parameteriserade funktionens argumentlista.
När man sedan använder den parameteriserade funktionen skriver man precis som vanligt:
int i, j;
char k;
...
Byt( i, j ); //OK
Byt( i, k ); //Error, olika typer.
Övningsuppgift:
· Om du har projektet byt kvar kan du öppna det, annars går det lika bra att skapa ett nytt projekt, eftersom detta blir mycket mindre att skriva än förra gången.
· Skapa funktionstemplate för byt() enligt ovan.
· Testa i main att byta variabler av olika typ.
· Testa även att byta två CText-objekt.
Man är inte bunden till att alla datatyper ska användas av denna mall. Om man t.ex. vill byta två texter av den gamla hederliga C-typen char[], fungerar inte tilldelningsoperatorn, och mallen skulle inte ge förväntat resultat. Det går att deklarera en ‘vanlig’ funktion med samma namn, där man anger ny datatyp. Man kan på så sätt ‘reservera’ denna datatyp till att särbehandlas av denna funktion. Då skulle man kunna använda strcpy() i den funktionen, och återställa förväntat resultat:
void Byt(char *a, char *b)
{
// Byt m.h.a. strcpy.
...
}
Övningsuppgift:
· Öppna projektet byt och lägg till en funktion som tar två char* som argument.
· Använd strcpy() i funktionen för att byta de två texterna.
· Prova i main() att byta två texter av typen char-listor. Texterna ska ha samma längd t.ex [81].
Klassmallar.
Man kan använda klassmallar till att skapa en grupp klasser som fungerar med olika typer. Vi testar att skapa en objektlista:
template <class T, int i>
class CObjektLista
{
public:
CObjektLista( void );
~CObjektLista( void );
int SetMedlem( T a, int b );
private:
T m_Lista[i];
int m_Antal;
};
I detta exempel använder objektlistan två argument, en typ T, och ett heltal i. För ‘T’ kan man ange ett objekt av valfri typ eller klass. Heltalet ‘i’ däremot måste vara ett konstant heltal. Nu när i är en konstant, vilken alltså är känd vid komplieringstillfället, kan man skapa en lista med i stycken element.
Till skillnad från funktionsmallar är man inte tvungen att använda alla mallargument i mallklassens definition.
När man deklarerar metoderna till en klassmall använder man ungefär samma syntax som vanligt. Förutom att kvalificera funktionen, d.v.s. använda räckviddsoperatorn till att ange vilken klass funktionen tillhör, föregås funktionen av en rad som talar om vilken template vi avser:
template <class T, int i>
Man anger även mallens argument mellan klassnamnet och räckvidsoperatorn. Konstruktor och destruktor skulle då kunna se ut ungefär så här:
template <class T, int i>
CObjektLista < T, i >::CObjektLista( void )
{
// Eventuell
konstruktionskod.
}
template <class T, int i>
CObjektLista < T, i >::~CObjektLista( void )
{
// Eventuell
destruktionskod.
}
Då skulle vi kunna deklarera metoden SetMedlem() på samma sätt:
template
<class T, int i>
int
CObjektLista<T, i>::SetMedlem(T a, int b)
{
if((b
>= 0) && (b < i))
{
m_Lista[b++] = a;
return sizeof(a);
}
else
{
return -1;
}
}
Till skillnad från funktionsmallar måste man ange argumenten för en klass när man skapar ett objekt. Så här kan man t.ex. skapa ett objekt baserat på ovanstående klass:
CObjektLista< float, 6 > test1; // OK
CObjektLista< char, antal++ > test2; // Fel, andra argumentet
// måste vara konstant.
Observera att medlemsfunktioner som inte används inte heller kommer med i exe-filen. Detta kan medföra problem om man skapar ett bibliotek med mallar för andra programmerare.
Man får vara försiktig med tecknen ‘>‘ och ‘<‘ när man använder mallar, eftersom de ingår i syntaxen. Om man t.ex. använder villkorsoperatorn som argument för ett heltal kan syntaxen bli helt fel:
CObjektLista< float, a > b ? a
: b > test3; // Fel!
Ovanstående kan rättas till genom användning
av paranteser:
CObjektLista< float, (a > b ?
a : b) > test3; // OK.
Liknande problem kan uppstå om man använder makron som argument till en mall, men då är det svårare att se vad som egentligen är fel.
? Collection Classes
Användningsområden.
Mallar används ofta för att:
· Skapa typsäkra samlingsklasser (collection classes) som kan arbeta med valfria datatyper. T.ex en lista som kan lagra ett antal objekt av valfri datatyp, dock måste alla element i en lista ha samma typ.
· Lägga till extra typkontroller för funktioner som annars skulle behöva ta void-pekare som argument.
· Kapsla in grupper av operatoröverlagringar för att ändra deras effekt på olika datatyper, t.ex. s.k. intelligenta pekare (smart pointers).
Det mesta av detta går att lösa på andra sätt, men mallar ger oss en del fördelar:
· Det är lättare att skriva en mall. Man skapar bara en mall för sin klass eller funktion i stället för att deklarera en för varje datatyp den ska kunna hantera.
· Användning av mallar höjer programmets abstraktionsnivå (se nästa kapitel), d.v.s. man kan tydligare förstå programmet när man läser källkoden.
· Mallar är typsäkra. När kompilatorn läser källkoden är datatyperna kända, och vanlig typkontroll utföres.
Man skulle kunna använda makron till att lösa många av de uppgifter som mallarna är avsedda att lösa åt oss. Det finns dock stora skillnader, och dessa skillnader är till fördel för mallar. Vi kan t.ex. jämföra nedanstående makro och mall:
#define MIN(i, j) (((i) < (j)) ? (i) : (j))
template<class T> T min (T i, T j) { return ((i < j) ? i : j) }
Vid en första anblick verkar det som om makrot skulle ge samma fördelar som mallen, men det finns en del problem med makrot:
· Kompilatorn har ingen möjlighet att avgöra om de argument som anges i programkoden är av matchande typ.
· Vi diskuterade tidigare vad som händer om man använder operatorerna ++ eller -- på argumenten. Skulle t.ex. i++ vara mindre än j++ kommer i att ökas två gånger.
· Makron tolkas före kompilering, och de felmeddelanden som eventuellt rapporteras relateras inte till makrot, ej heller till vår tillämpning av makrot, utan till den form makrot har efter att preprocessorn tolkat den åt oss. Samma problem slår igenom vid avlusning.
Många funktioner som tidigare krävde användning av typlösa pekare kan nu i stället skapas m.h.a. mallar. Typlösa pekare används ofta för att möjliggöra att en funktion arbetar med argument av okänd typ. Kompilatorn kan därför varken utföra normal typkontroll, tillämpa typspecifika operatoröverlagringar eller anropa konstruktor eller destruktor för klassbaserade objekt.
Allt detta finns tillgängligt när man använder mallar. I malldefinitionen syns inte vilken datatyp man senare kommer att använda, men vid kompileringstillfället är datatypen angiven, och ett korrekt objekt skapas, vilket kan ses vid avlusning etc. Kom ihåg att varje gång man anropar en mallfunktion eller skapar ett objekt baserat på en mallklass och använder en datatyp man inte använt tidigare, skapas en ny funktion eller klass avsedd endast för angiven datatyp. Denna har sedan exakt samma status som om vi explicivt hade deklarerat funktionen eller klassen för angiven datatyp.
Mallar passar utmärkt för att skapa listklasser, ‘collection classes’. I MFC finns ett antal sådana: CArray, CMap, CList, CTypedPtrArray, CTypedPtrList, och CTypedPtrMap. Exempel på användning av en sådan finns i självstudien ‘Scribble’, vilken rekommenderas i slutet av denna kurs.
Ett exempel.
Hjälpen innehåller ett par exempel, av vilka vi ska studera ett här.
MyStack:
Klassen MyStack är en enkel tillämpning av en stack, en sist-in-först-ut-lista innehållande data av en viss typ, vilken anges först när man skapar ett objekt. De två argumenten ‘T’ och ‘i’ anger vilken datatyp som kan lagras respektive maximalt antal lagrade element. Metoderna ‘push’ och ‘pop’ används för att lagra respektive hämta från stacken.
template <class T, int i>
class MyStack
{
T StackBuffer[i];
int cItems;
public:
void MyStack( void ) : cItems( i ) {};
void push( const T item );
T pop( void );
};
template <class T, int i>
void MyStack<T, i>::push(const T item)
{
if( cItems > 0 )
{
StackBuffer[--cItems] = item;
}
else
{
throw "Stack overflow error.";
}
return;
}
template <class T, int i>
T MyStack<T,i>::pop(void)
{
if( cItems < i )
{
return StackBuffer[cItems++];
}
else
{
throw "Stack underflow error.";
}
}
RefCount:
I C++ kan man skapa klasser med ‘intelligenta pekare’, vilka kapslar in pekare och överlagrar pekaroperatorer för att omdefiniera de funktioner som kan utföras via pekare. Mallar låter oss tillverka standardpaket vilka kan kapsla in pekare av nästan vilken typ som helst.
Nedanstående exempel demonstrerar ett sätt att skapa en lista för objekt av diverse typer, inkluderande en referensräknare. Mallklassen Ptr<T> hanterar en lista med diverse pekare vilka fungerar mot varje klass som ärvt från klassen RefCount.
class RefCount
{
int crefs;
public:
RefCount(void) { crefs = 0; }
void upcount(void) { ++crefs; }
void downcount(void) { if (--crefs == 0) delete this; }
};
class Sample : public RefCount
{
public:
void doSomething(void) { TRACE("Did something\n");}
};
template <class T> class Ptr
{
T* p;
public:
Ptr(T* p_) : p(p_) { p->upcount(); }
~Ptr(void) { p->downcount(); }
operator T*(void) { return p; }
T& operator*(void) { return *p; }
T* operator->(void) { return p; }
Ptr& operator=(T* p_)
{
p->upcount(); p = p_; p->downcount(); return *this;
}
};
int main()
{
Ptr<Sample> p = new
Sample; //
sample #1
Ptr<Sample>
p2 = new Sample; // sample #2
p = p2; //
#1 has 0 refs, so it is destroyed;
//
#2 has two refs
p->doSomething();
return 0;
// As p2
and p go out of scope, their
//
destructors call downcount. The cref
// variable
of #2 goes to 0, so #2 is
//
destroyed
}
Klasserna RefCount och Ptr<T> skapar tillsammans en möjlighet att ha en lista med pekare till objekt av diverse typer. Förutsättningen är att de olika typerna ärver från klassen RefCount, vilket i sig utgör ett tillägg av ett heltal. Lägg märket till fördelen med att använda Ptr<T> i stället för en vanlig pekarklass, vi får en helt typsäker lista. Ovanstående kod låter oss använda ett objekt av Ptr<T> i nästa alla de fall där man kunnat använda en pekare T*, medan en vanlig listklass endast skulle ge oss en typomvandling till en typlös pekare (void*).
Man kan t.ex. använda denna klass till att hantera diverse objekt, t.ex. filer, texter etc. Tack vare mallklassen Ptr<T> skapar kompilatorn klasserna Ptr<File>, Ptr<String> etc. Medlemsfunktioner som Ptr<File>::~Ptr(), Ptr<File>::operator File*(), Ptr<String>::~Ptr(), Ptr<String>::operator String*() etc. skapas också av kompilatorn.