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 dek­larera en funktion/klass för varje datatyp som den ska fungera med. Templa­tes är en mekanism som möjliggör att man deklarerar typparametrar för detta än­damå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 ‘parameter­iz­ed 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 funk­tion 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 (tem­plate 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ång­en använder mallen i programmet, och då är det en speciell version, av funkti­o­n­en eller klassen baserad på angivna datatyper, som skapas. Denna version åter­används om man använder mallen fler gånger med samma datatyper, så den kom­mer 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 argu­ment som används i källkoden. I andra hand försöker den använda den deklare-r­ade 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 regler­na 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 ex­emp­let ‘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 val­fri 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å vari­abler 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 an­givna 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 data­typ. Man kan på så sätt ‘reservera’ denna datatyp till att särbehandlas av den­na funktion. Då skulle man kunna använda strcpy() i den funktionen, och åter­stä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 mall­argument 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äck­vidds­operatorn 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å ovan­stå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, efter­som de ingår i syntaxen. Om man t.ex. använder villkorsoperatorn som argu­m­ent 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 mal­larna är avsedda att lösa åt oss. Det finns dock stora skillnader, och dessa skill­na­der ä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 an­ropa 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 kompilerings­till­fället är datatypen angiven, och ett korrekt objekt skapas, vilket kan ses vid av­lusning 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 tidi­gare, skapas en ny funktion eller klass avsedd endast för angiven datatyp. Den­na har sedan exakt samma status som om vi explicivt hade deklarerat funktion­en 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 ‘Scrib­ble’, 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.