*:85/ID200V C++ HT07 Föreläsning 12-14 Mallar Generiska enheter - mallar (templates) Upprepning En mall i C++ är ett källkodsmönster för en klass- eller funktionsdefinition, där någon eller några specifika typer (eller värden) har ersatts med formella parametrar. Vid användning av mallen anges vika aktuella typer som skall användas, varvid kompilatorn genererar motsvarande klass- eller funktionsdefinition och kompilerar den (instansiering). Denna mekanism kan (speciellt i C++) liknas med en preprocessor som gör en textuell ersättning av de formella parametrarna med aktuella argument. Kompilatorn förstår dock mallar och kan i viss mån kontrollera deras syntax. Mekanismen kan även ses som en ökning av abstraktionsnivån: klasser utgör abstraktioner över objekt, men klassmallar utgör abstraktioner över klasser. Andra viktiga språk som understödjer generiska enheter är t.ex. Ada och Eiffel. Det finns även varianter av Java med generiska enheter, t.ex. GJ (Generic Java) Genom mallar stödjer C++ generisk programmering som är en annan programmeringsparadigm än objektorienterad programmering och erbjuder i många fall alternativa sätt för lösning av samma typer av problem. Bild 162 1
Abstraktionsnivåer Mallar Klasser (eller funktioner), alltså typer Objekt (eller anrop) Bild 163 Funktionsmallar - exempel // class kan användas istället för typename void swap(typ& x, TYP& y){ TYP slask=x; x=y; y=slask; Ur denna funktionsmall kan kompilatorn framställa funktionsdefinitioner för byte mellan variabler av godtycklig (men samma) typ för vilken tilldelning är tillåten. Genereringen av en funktions- eller klassdefinition ur en mall för ett visst mallargument kallas instansiering av mallen. Den genererade definitionen kallas en specialisering. Instansieringen för funktioner är implicit - man behöver (oftast) inte ange de aktuella mallargumenten. När kompilatorn ser anropet av en funktion med detta namn gissar den mallargumenten från argumenttypen i anropet: int main(){ int i1=13, i2=127, i3=173, i4=14; double d1=3.5, d2=7.3; swap(i1, i2); // swap(int&, int&) genereras swap(d1, d2); // swap(double&,double&) genereras swap(i3, i4); // inget genereras, swap(int&,int&) finns redan, anropas swap(i1, d1); // Fel, det finns ingen mall för swap med olika argumenttyper Bild 164 2
Klassmallar - exempel: mall för en vektorklass #ifndef VECTOR_H #define VECTOR_H class Vector{ int siz, cap; TYP *arr; Vector():siz(0), cap(10), arr(new TYP[cap]){ Vector(const Vector& other); // Andra konstruktorer, destruktorn, tilldelning TYP& operator[](int pos){ return arr[pos]; const TYP operator[](int pos) const { return arr[pos]; void push_back(const TYP& val); void pop_back(){ siz--; int size() const { return siz; TYP max() const; // Andra vektoroperationer ; Som för vanliga klasser bör större medlemsfunktioner definieras utanför klassdefinitionen, men Bild 165 Vector<TYP>::Vector(const Vector<TYP>& other): siz(other.siz), cap(other.cap), arr(new TYP[cap]){ for(int i=0; i<siz; i++) arr[i] = other.arr[i]; template <typename T> void Vector<T>::push_back(const T& val){ if (siz==cap){ T *tmp=new T[cap*=2]; for(int i=0; i<siz; i++) tmp[i]=arr[i]; delete [] arr; arr=tmp; arr[siz++]=val; TYP Vector<TYP>::max() const { if (siz == 0) throw std::length_error( Fel! ); TYP m = arr[0]; for(int i=1; i<siz; i++) if (arr[i] > m) m = arr[i]; return m; #endif Mallar för definitioner av medlemsfunktioner eftersom de är medlemmar i en mall så blir deras definitioner också mallar! Dessutom måste även definitionerna finnas med i headerfilen - mer om detta senare. Obs att mallparameterns namn bara har lokal betydelse i varje mall Bild 166 3
Användning av vektormallen i en tillämpning #include Vector.h // Säg att Vector-mallen finns här #include string using namespace std; class Person{ ; int main(){ Vector<int> ivec; Vector<string> svec; ivec.push_back(13); svec.push_back( Stefan ); string sm = svec.max(); int im = ivec.max(); Instansiering av klassmallar är explicit - mallargumenten måste anges. Medlemsfunktioner instansieras först vid kompilering av ett program som anropar dem! Vector<Person> pvec; // Förutsätter att Person har en default-ktor pvec.push_back(person( Jozef, 53)); pvec.push_back(person( Stefan, 61)); Person pm = pvec.max(); // Fel, instansiering av // Vector<Person>::max // misslyckas, det finns ingen operator> // för Person! Bild 167 Obegränsad genericitet I många mallar måste man anta att vissa operationer kommer att vara definierade för de typer mallen kan instansieras för. Det finns inget sätt i C++ att ange sådana restriktioner för de typer mallen skall kunna instansieras för. Instansieras mallen i tillämpningen med en typ som saknar någon operation mallen förutsätter så blir det kompileringsfel vid instansiering av den funktion som använder operationen, med felmeddelanden gällande den felaktiga koden i mallen, inte i tillämpningen. Å andra sidan kan man använda sådana mallar med fel typer så länge man inte använder de medlemsfunktioner som förutsätter den saknade operationen. Vi skall senare se hur man kan parametrisera mallar även med operationer. I andra språk som använder genericitet brukar man kunna ange vilka operationer som måste vara definierade för typer mallen instansieras för. I dessa språk får man alltså felmeddelanden vid instansiering av mallar med fel typer. Det finns även förslag på införande av sådana mekanismer i nästa C++-standard. Bild 168 4
Mer om klassmallar Det finns nu ingenting som heter Vector, överallt där man vill använda mallen måste den specialiseras, antingen med en specifik typ, t.ex. Vector<int> eller med mallargumentet till den enhet som använder Vector<TYP> - denna måste alltså enhet också vara en mall: void print_vector(const Vector<TYP>& vec){ for(int i = 0; i < vec.size(); i++) cout << vec[i] << endl; // Förutsätter att << är definierad för TYP Obs att mallighet är smittsam : en funktion som skall kunna ta en godtycklig specialisering av en mall som ett argument måste själv vara en mall, om funktionen är en medlemsfunktion i en klass så måste klassen vara en mall (inte riktigt sant, se senare om medlemsfunktionsmallar), en klass som skall ha som medlem en godtycklig specialisering av en klassmall måste själv vara en mall o.s.v. Bild 169 Mer om mall- och typnamn Inuti en klassmall kan dock namnet utan mallargumenten, t.ex. Vector, användas som en förkortning för en instansiering med samma argument, t.ex. Vector<TYP> Exempelvis kan definitionen av copy-konstruktorn göras så här Vector<TYP>::Vector(const Vector& other): siz(other.siz), cap(other.cap), arr(new TYP[cap]){ for(int i=0; i<siz; i++) arr[i] = other.arr[i]; Deklarationer med klassmallar kan bli ganska grötiga, man använder därför ofta typedefs för specialiseringar man använder frekvent: typedef Vector<int> Intvector; typedef Vector<string> Stringvector; T.ex. är standardbibliotekets string egentligen bara en typedef: typedef basic_string<char, char_traits, alloc<char> > string; Den genererade klassen har inte längre tillgång till namnet på den typ mallen har specialiserats för. Om man vill kunna få fram denna typ kan man lägga in en typedef i mallen: class Vector{ typdedef TYP value_type; Vi kommer senare att se behov av sådana konstruktioner Bild 170 5
Definitioner i headerfiler! Eftersom mallar instansieras först vid kompilering av program som använder dem måste deras definitioner finnas tillgängliga för kompilatorn vid kompilering av tillämpningsprogrammet. Detta innebär att definitioner av funktionsmallar och medlemsfunktionsmallar måste finnas i headerfiler (de följer samma regler som inline-funktioner). Det hindrar inte att man fortfarande gör (mallar för) klassdefinitioner först och placerarar (mallar för) medlemsfunktioner därefter, utanför (mallen för) klassdefinitionen. Om man vill kunna kontrollera syntaxen i sina mallar genom kompilering kan skapa en.cpp-fil som bara inkluderar headerfilen och kompilera den, man kan även temporärt döpa om headerfilen till ett namn med filtypen.cpp och kompilera. Obs dock att det bara är begränsad syntaxkontroll kompilatorn kan göra på oinstansierade mallar - den vet ju inte för vilka typer mallen kommer att instansieras. Bild 171 Icke-typ-mallparametrar Mallparametrar behöver inte vara typer, de kan vara värden - argumentvärdet vid instansiering måste då vara ett konstant uttryck. template<typename TYP, int SIZE> class Buffer{ int count; TYP arr[size]; Buffer():count(0){ ; int main(){ Buffer<char, 256> cbuf; Buffer<string, 10> sbuf; Obs att detta värde kompileras in, cbuf och sbuf är objekt av två helt olika klasser med konstanta storlekar på sina arrayer. Bild 172 6
Mallspecialiseringar är olika typer Mallspecialiseringar är helt olika typer som inte har något med varandra att göra. Exempel: Klass<> class Klass{ TYP data; Klass<string> Klass<int> Klass(TYP d):data(d){ void visa() const { cout << data << endl; ks ki ; 42 int main(){ Klass<std::string> ks("how many roads must a man walk down?"); Klass<int> ki(42); ks.visa(); ki.visa(); Ovanstående går bra. Men om vi vill ha en samling av sådana objekt (med olika typer på data) för att gå igenom dem och anropa visa() för varje objekt så går det inte eftersom objekten är av olika typer. Obs! att medlemsfunktionen visa() ser olika ut i Klass<string> och Klass<int>, det är olika operator<< som anropas! How many roads must a man walk down? Bild 173 Basklasser till klassmallar Om man vill att objekt av klasser som genereras ur en mall skall kunna hanteras som hörande till samma typ (t.ex. pekas ut från samma datastruktur) måste de utrustas med en gemensam basklass (som inte är en mall): class Klass_base{ virtual void visa() const = 0; ; class Klass:public Klass_base{ TYP data; Klass(TYP d):data(d){ void visa() const { cout << data << endl; ; int main(){ Klass_base *arr[2]; arr[0]=new Klass<std::string>("How many roads was it?"); arr[1]=new Klass<int>(42); for(int i=0; i<2; i++) arr[i]->visa(); Den gemensamma typen är Klass_base. Obs att de funktioner som skall kunna anropas utan vetskap om typen måste virtual-deklareras i basklassen. Obs att Klass_base inte kan innehålla något som är beroende av mallargumentet (typen). Obs att objekten ny är polymorfa och bör hanteras via pekare (motsv.) Bild 174 7
Exempel ur inluppen I inluppen kommer man att vilja representera kopplingar till tillämpningens okända objekt av okänd typ och till en okänd medlemsfunktion i objektet. Detta görs med en pekare till objektet och en medlemspekare till medlemsfunktionen, men eftersom objektets typ är okänt måste det göras i en mall. Men sedan vill man kunna ha samlingar av sådana kopplingar - lösningen: en basklass: class Action_base{ virtual void perform() = 0; ; class Action : public Action_base { TYP *obj; void (TYP::*funk)(); Action(TYP *o, void(typ::*f)()):obj(o), funk(f){ void perform(){ (obj->*funk)(); ; Bild 175 Exempel ur inluppen, forts. Där man vill ha ett godtyckligt Action-objekt kan man nu ha en Action_base *: class Button : public Component{ std::string caption; Action_base *action; Button(, string cap, Action_base *a):, caption(cap), action(a){ bool handle_event(event eve){ if (eve.what == terminal::return) { action->perform(); return true; else return false; Tillämpningen kan då skapa sina knappar med en koppling till en medlemsfunktion i sina objekt enligt följande exempel: Button* b=new Button(, new Action<Value>(&v, &Value::oka)); (något förändrat vid användning av referensräknande smarta pekare ) Bild 176 8
Hjälpfunktioner för implicit instansiering av klassmallar Instansiering av funktionsmallar är implicit - kompilatorn gissar sig till mallargumenten ur funktionsanropet ( argumentdeduktion ). Instansiering av klassmallar är explicit - mallargumentet måste anges efter klassnamnet: Button *b = new Button(, new Action<Value>(&v, &Value::oka)); För att underlätta skapande av objekt av mallklasser brukar biblioteksskapare tillhandahålla funktionsmallar för skapande av objekten - dessa funktionsmallar blir då bekvämare att använda: Action<TYP>* mk_action(typ *obj, void (TYP::*funk)()){ return new Action<TYP>(obj, funk); Tillämpningen kan nu skapa sina Action<>-objekt lite bekvämare: Button* b = new Button(, mk_action(&v, &Value::oka)); Bild 177 Argumentdeduktion för funktionsmallar Vid implicit instansiering av funktionsmallar försöker kompilatorn deducera mallargumenten (typerna) ur typer för aktuella argumentvärden i funktionsanropet. Funktionens argument behöver inte vara av de typer som utgör mallargument, det räcker att mallargumenten framgår av funktionens argument. Exempel: template<typename TYP> void funk(typ *pek){ TYP temp = *pek; ; int *ipek; funk(ipek); Funktionsargumentet är en int * så mallargumentet är int template<typename TYP, int SIZ> void funk(typ (&arr)[siz]){ for(int i; i<siz; i++) cout << arr[i] << endl; ; char buff[128]; funk(buff); Funktionsargumentet är en char[128] så mallargumentet TYP är char och mallargumentet siz är 128 Bild 178 9
Explicit instansiering av funktionsmallar Ibland framgår inte mallargumenten ur funktionsargument. Detta inträffar då mallargumentet (typen) används som returtypen för funktionen, men inte som funktionsargumenttyp (returtypen deltar inte i deduktionen eftersom det inte alltid framgår av anropet vad den skall vara). I så fall måste mallargument anges explicit vid anropet: template <typename RET, typename ARG> RET funk(const ARG& param) { RET x; return x; string str; str = funk<string, int>(13); De mallargument som kan deduceras behöver inte anges om de står sist i listan: str = funk<string>(13); Explicit instansiering av funktionsmallar kan även användas för att påtvinga en instansiering där de aktuella funktionsargumenten inte exakt överenstämmer med mallargumenten (för att påtvinga en implicit konvertering): str = funk<string, double>(13); Bild 179 Ingen konvertering vid argumentdeduktion När en funktionsmall skall instansieras kollar kompilatorn om det redan finns en instans av funktionen med samma argumenttyper. Vid denna kontroll används dock inte de vanliga typkonverteringsmekanismerna. template<typename TYP> TYP add(typ x, TYP y){ return x + y; Triviala konverteringar som array till pekare, funktion till funktionspekare, TYP till const TYP används dock. int i1, i2, i3; short sh1, sh2, sh3; long lo1, lo2, lo3; i3 = add(i1, i2); sh3 = add(sh1, sh2); lo3 = add(lo1, lo2); i1 = add(25, i3); // add(int, int) genereras // add(short, short) genereras // add(long, long) genereras // finns redan, används Bild 180 10
Överlagrade funktionsmallar Funktionsmallar kan överlagras med andra funktionsmallar och med vanliga funktioner. Det är den mest specialiserade funktionen som kommer att anropas (efter eventuell instansiering). void funk(typ x){ cout << funk<typ> << endl; Vi återkommer till specialiserade klassmallar void funk(typ *x){ cout << funk<typ *> << endl; senare void funk(int x){ cout << funk<int> << endl; int main(){ int i=15; int *ipek=&i; string str( Jozef ); funk(i); // Skriver funk<int> funk(ipek); // Skriver funk<typ *> funk( Jozef ); // Skriver funk<typ *> funk(str); // Skriver funk<typ> Bild 181 Överlagring av funktionsmallar, exempel Ibland inträffar det att en generell mall är bra för de flesta men inte alla typer, d.v.s. att man för en viss specifik typ (eller familj av typer) vill ange ett alternativt källkodsmönster. Man kan då skapa alternativa mallar som kompilatorn skall välja om mallen instansieras med denna specifika typ (eller en typ från denna familj av typer). Exempel: generell mall för swap-operationen: void swap(typ& x, TYP& y){ TYP slask=x; x=y; y=slask; För byte mellan två Vector<>-objekt med värdesemantik (bl. a. överlagrad operator=) är detta ineffektivt, elementarrayer allokeras, kopieras och avallokeras i onödan. Man kan då skapa en mall för swap som är mer specialiserad än den generella mallen genom att ange funktionsargumenten som referenser till Vector<>specialiseringar. void swap(vector<typ>& x, Vector<TYP>& y) { int s = x.siz, c = x.cap; TYP *a = x.arr; x.siz = y.siz; x.cap = y.cap; x.arr = y.arr; y.siz = s; y.cap = c; y.arr = a; Obs! Fel, försöker jobba på privata data - se nästa bild Bild 182 11
Rättelse till föregående bild Mallen för swap-funktionen på förra bilden är felaktig eftersom en Vectors data är privata och swap-funktionen inte kan komma åt dem. Detta kan lösas genom att swap-funktionen friend-deklareras i Vector (måste göras på ett speciellt sätt, se bild 186) eller att Vector-klassen utrustas med en medlemsfunktion för byte med ett annat Vector-objekt och denna funktion anropas från mallen (så görs det i STL): class Vector{ void swap(vector& other); ; template<typename TYP> void Vector<TYP>::swap(Vector& other){ int s=siz, c=cap; TYP *a = arr; siz=other.siz; cap=other.cap; arr=other.arr; other.siz=s; other.cap=c; other.arr=a; void swap(vector<typ>& x, Vector<TYP>& y){ x.swap(y); Bild 183 Exempel från inluppen Man kan tänka sig att Action<>-objekt även borde kunna knytas till fria funktioner och till medlemsfunktioner som kan ta t.ex. en knapps textsträng som argument. Man kan inte ha flera klasser med samma namn, så klassmallar kan inte överlagras, vi får hitta på nya namn för sådana klasser: class Action_arg : public Action_base { std::string arg; TYP *obj; void (TYP::*funk)(std::string); Action_arg(std::string a, TYP *o, void(typ::*f)(std::string)): arg(a), obj(o), funk(f){ void perform(){ (obj->*funk)(arg); ; class Action_free : public Action_base { // Inte ens en mall void (*funk)(); Action_free(void(*f)()):funk(f){ void perform(){ funk(); ; Bild 184 12
Exempel från inluppen, forts. Vi kan dock underlätta för tillämpningsprogrammerare genom att skapa överlagrade funktionsmallar för hjälpfunktioner: template <typename T> Action_arg<T>* mk_action(string a, T *o, void (T::*f)(string)){ return new Action_arg<T>(a, o, f); Action_free* mk_action(void (*funk)()){ return new Action_free(funk); Tillämpningen kan nu skapa alla sina Action<>-objekt på samma sätt : void quit() { std::exit(0); Button* b1=new Button(, mk_action(&v, &Value::oka)); Button* b2=new Button(.., mk_action( Minska,&v,&Value::minska)); Button *b3=new Button(, mk_action(quit)); Bild 185 friend-deklarationer i klassmallar Med en friend-deklaration i en klassmall kan man mena tre olika saker: 1. vännen är en viss specifik funktion eller klass 2. vännen är en instans av en funktions- eller klassmall instansierad med samma typ (-er) som denna klass I detta fall måste deklarationer av dessa funktions- eller klassmallar finnas inom deklarationsvidden, före klassmallen som deklarerar dem som vänner Speciell syntax (se nästa bild). 3. vännen är en godtycklig instans av en viss funktions- eller klassmall. I detta fall blir friend-deklarationen i klassen en mall i sig: class Klass{ template <typename TYP2> friend void funk(const TYP2&); Bild 186 13
friend-deklarationer i klassmallar, exempel Exempel på fall 2 från föregående bild: säg att vi vill lösa problemet med den specialiserade swap-funktionens åtkomst till Vectors privata data genom att deklarera swap-funktionen som vän: template <class TYP> void swap(typ& x, TYP& y); template <class TYP> class Vector; template <class TYP> void swap(vector<typ>& x, Vector<TYP>& y); Den specialiserade friend-deklarationen kräver att mallen för funktionen redan är deklarerad. Men deklaration av mallen kräver att Vector<> redan är deklarerad template <class TYP> class Vector{ friend void swap<typ>(vector<typ>&, Vector<TYP>&); ; template <class TYP> void swap(vector<typ>& x, Vector<TYP>& y){ int s=x.siz, c=x.cap; TYP *a = x.arr; x.siz=y.siz; x.cap=y.cap; x.arr=y.arr; y.siz=s; y.cap=c; y.arr=a; Bild 187 Medlemsmallar I standard-c++ kan man göra medlemmar i en klass (eller klassmall) till mall. Syntaktiskt exempel: class Klass{ string namn; Klass(string n):namn(n){ ; template <typnamn TYP> void skriv(const TYP& meddel) const { cout << namn << : << meddel; int main(){ Klass teacher( Jozef ); const string tidsenh( minuter ); teacher.skriv( Vi tar paus på ); teacher.skriv(17); teacher.skriv(tidsenh); Bild 188 14
Medlemsmallar, mer realistiskt exempel Säg att vi i vår Vector<>-mall vill ha en konstruktor som initierar vektorn med värden som tas från vilken annan typ av behållare som helst, bara elementen är av rätt typ och kan pekas ut av pekare (eller något som beter sig som pekare): class Vector{ int siz, cap; TYP *arr; template <typename PEKTYP> // Deklaration Vector(PEKTYP pek1, PEKTYP pek2); ; // Definition template <typename PEKTYP> Vector<TYP>::Vector(PEKTYP pek1, PEKTYP pek2): siz(0), cap(10), arr(new TYP[cap]) { while (pek1!= pek2) push_back(*pek1++); Bild 189 Medlemsmallar, forts. Exempel på användning: #include <list> #include Vector.h int main(){ list<int> li; int arr[] = {13, 7, 78, 53, 92; const int arrsize = sizeof(arr) / sizeof(int); for(int i=0; i<arrsize; i++) li.push_back(arr[i]); Vector<int> v1(li.begin(), li.end()); // Vector<int>::Vector(list<int>::iterator,list<int>::iterator) Vector<int> v2(arr, arr+arrsize); // Vector<int>::Vector(int *, int *) Bild 190 15
Konvertering mellan mallspecialiseringar I vissa speciella situationer vill man att objekt av olika klasser genererade ur samma mall skall kunna konverteras till varandra. Detta inträffar t.ex. för mallar för referensräknande pekarklasser som skall användas för att peka ut objekt ur en klasshierarki: ett pekarobjekt för basklassen skall kunna tilldelas från ett pekarobjekt för subklassen enligt de vanliga reglerna för basklass-/subklass-pekare. För att möjliggöra detta får man i mallen för pekarklassen skapa en mall för konverteringsoperatorer. Först en repetition av konverteringsoperatorer: class Alfa{ ; class Beta{ operator Alfa() { // Detta är en konverteringsoperator som anger // hur man gör ett Alfa-objekt ur Beta-objektet ; Bild 191 Konvertering mellan mallspecialiseringar, forts. Om det istället är fråga om konverteringar mellan två klasser genererade ur samma mall för man göra en mall för konvertering till specialisering för en annan typ: class Alfa{ template <typename ANNANTYP> operator Alfa<ANNANTYP>(); ; template <typename ANNANTYP> Alfa<TYP>::operator Alfa<ANNANTYP>(){ return ett temporärt Alfa<ANNANTYP>-objekt konstruerat ur detta Alfa<TYP>-objekt Bild 192 16
Mall för pekarklass för polymorfa objekt class null_pointer; // Undantagsklass, visas senare template <typename T> class Ptr{ T *ptr; int *count; template<typename U> friend class Ptr; Ptr(T *p, int *c); // Privat konstruktor, används vid konvertering Ptr(T *p=0) throw(std::bad_alloc); Ptr(const Ptr& other) throw(); ~Ptr() throw(); const Ptr& operator=(const Ptr& other) throw(); T& operator*() throw(null_pointer); const T& operator*() const throw(null_pointer); T* operator->() throw(null_pointer); const T* operator->() const throw(null_pointer); bool operator==(const Ptr& other) const throw(); bool operator!=(const Ptr& other) const throw(); operator T*() const throw(); // Konvertering till vanlig pekare template <typename OTHERTYPE> // Konvertering till Ptr för annan operator Ptr<OTHERTYPE>(); // typ (basklasstyp) ; Bild 193 Implementering av vissa medlemmar i Ptr-mallen template <typename T> // Den nya privata konstruktorn Ptr<T>::Ptr(T *p, int *c):ptr(p), count(c){ if (ptr) (*count)++; template <typename T> // Den publika konstruktorn Ptr<T>::Ptr(T *p) throw(std::bad_alloc): ptr(p), count(ptr?new int(1):0){ // Konverteringsoperator till Ptr för annan typ template <typename OTHERTYPE> Ptr<TYP>::operator Ptr<OTHERTYPE>(){ Se OH 100-101 return Ptr<OTHERTYPE>(ptr, count); för implementering av andra medlemmar template <typename T> // Konverteringsoperator till vanlig pekare Ptr<T>::operator T*() const throw(){ return ptr; Bild 194 17
Implementering av vissa medlemmar i Ptr-mallen, forts. template <typename T> T& Ptr<T>::operator*() throw(null_pointer){ if (ptr == 0) throw null_pointer(); return *ptr; template <typename T> T* Ptr<T>::operator->() throw(null_pointer){ if (ptr == 0) throw null_pointer(); return ptr; const-varianter implementeras på samma sätt Kräver #include <stdexcept> class null_pointer : public std::runtime_error{ null_pointer() : runtime_error("null_pointer"){ null_pointer(std::string msg): runtime_error("null_pointer" + msg){ ; Bild 195 Kommentarer till Ptr-mallen En bra och generell mall för referensräknande pekarklasser är svår att utforma (vilket är anledningen till att någon sådan inte finns i standardbiblioteket). Som någon har sagt: smarta pekare kan vara riktigt smarta med de kan inte vara riktiga pekare. Det kan uppkomma problem med användning av den föreslagna Ptr-mallen som jag inte inser nu, jag kommer i så fall att meddela problemen i FC (och förhoppningsvis en lösning). Ett problem som jag ser nu är att dynamic_cast inte fungerar på dessa pekarklasser, dynamic_cast vill ju ha en vanlig pekare att testa det utpekade objektet med. Detta kan lösas på många sätt men inget som är transparent för användaren. Vid behov återkommer jag i FC med förslag på en lösning. Bild 196 18
En polymorfisk klass för användning med Ptr class Bird : public Animal{ Fabriksfunktion static Ptr<Bird> create(string name, double ws); double get_wingspan() const; protected: Bird(string name, double ws); private: double wingspan; Bird(const Bird& other); const Bird& operator=(const Bird& other); Bird* operator&(); const Bird* operator&() const; ; Ptr<Bird> Bird::create(string name, double ws){ return Ptr<Bird>(new Bird(name, ws)); Bird::Bird(string name, double ws): Animal(name), wingspan(ws) { Påtvinga användning av create() för objektskapande, tillåt subklasser Förbjud kopiering, adresstagning osv. Implementeras inte! Det räcker att det görs i rotklassen double Bird::get_wingspan() const { return wingspan; Bild 197 Minns klassmallen Vector<>? Åter till mallar - klassmallar #ifndef VECTOR_H #define VECTOR_H class Vector{ int siz, cap; TYP *arr; Vector():siz(0), cap(10), arr(new TYP[cap]){ Vector(const Vector& other); // Andra konstruktorer, destruktorn, tilldelning TYP& operator[](int pos){ return arr[pos]; const TYP operator[](int pos) const { return arr[pos]; void push_back(const TYP& val); void pop_back(){ siz--; int size() const { return siz; TYP max() const; // Andra vektoroperationer ; Bild 198 19
Vector<>, definitioner av medlemsfunktioner Vector<TYP>::Vector(const Vector<TYP>& other): siz(other.siz), cap(other.cap), arr(new TYP[cap]){ for(int i=0; i<siz; i++) arr[i] = other.arr[i]; template <typename T> void Vector<T>::push_back(const T& val){ if (siz==cap){ T *tmp=new T[cap*=2]; for(int i=0; i<siz; i++) tmp[i]=arr[i]; delete [] arr; arr=tmp; arr[siz++]=val; TYP Vector<TYP>::max() const { if (siz == 0) throw std::length_error( Fel! ); TYP m = arr[0]; for(int i=1; i<siz; i++) if (arr[i] > m) m = arr[i]; return m; #endif Bild 199 Explicit mallspecialisering En generell mall anger källkodsmönstret som är parametriserat med (oftast) typer. Den kan instansieras med en godtycklig specifik typ, en sådan mallinstans kallas en specialisering. Ibland inträffar det att det generella källkodsmönstret är bra för de flesta men inte alla typer, d.v.s. att man för en viss specifik typ (eller familj av typer) vill ange ett alternativt källkodsmönster. Man kan då skapa ett alternativt källkodsmönster som kompilatorn skall välja om mallen instansieras med denna specifika typ (eller en typ från denna familj av typer). Detta kallas explicit specialisering eller användardefinierad specialisering. Det kan jämföras med funktionsöverlagring där kompilatorn väljer den funktion som bäst passar argumenttyperna vid ett anrop. För funktionsmallar har vi sett hur det kan göras genom överlagring med mer specialiserade mallar eller t.o.m. med vanliga funktioner (se OH 181-183). För klassmallar finns det speciell syntax och möjlighet till (fullständig) explicit specialisering av hel klass (fullständig) explicit specialisering av enskilda medlemmar partiell explicit specialisering Bild 200 20
Fullständig explicit specialisering av hel klass Antag följande klassmall (syntaktiskt exempel): /* alfa.h */ #ifndef ALFA_H #define ALFA_H template <typename T> class Alfa{ T data; Alfa(const T& d); bool operator<(const Alfa<T>& other) const; ; template <typename T> Alfa<T>::Alfa(const T& d):data(d){ template <typename T> bool Alfa<T>::operator<(const Alfa<T>& other) const{ return data < other.data; #endif Bild 201 Fullständig explicit specialisering av hel klass, forts. #include alfa.h int main(){ Alfa<int> ai1(3), ai2(17); if (ai1 < ai2) // Funkar bra. Alfa<char *> as1( stefan ), as2( jozef ); if (as1 < as2) // Funkar dåligt, jämför pekare!. Man kan göra en explicit specialisering av klassen Alfa för typen char *, se nästa bild. Bild 202 21
Fullständig explicit specialisering av hel klass, forts. /* alfa.h */ #ifndef ALFA_H #define ALFA_H // Precis som på bild 216 template<> // Anger att det är en specialisering class Alfa<char *>{ char* data; Alfa(const char *str); bool operator<(const Alfa<char *>& other) const; ~Alfa(); ; #endif #include "alfa.hpp" #include <cstring> using namespace std; Definitioner av medlemsfunktionerna är nu vanliga funktioner och måste göras i en.cpp-fil Alfa<char *>::Alfa(const char *str):data(new char[strlen(str)+1]){ strcpy(data, str); bool Alfa<char *>::operator<(const Alfa<char *>& other) const{ return strcmp(data, other.data)<0; Alfa<char *>::~Alfa() { delete [] data; Bild 203 Explicit specialisering av klassmallmedlemmar Enskilda medlemsfunktioner i en klassmall kan specialiseras. Exempel: medlemsfunktionen max() kommer inte att fungera väl för Vector<char *> eftersom den då kommer att jämföra pekare - kan ersättas med en max-funktion som använder strcmp: template<> char * Vector<char *>::max() const { char *m = arr[0]; for(int i=1; i<siz; i++) if (strcmp(arr[i], m) > 0) m = arr[i]; return m; Struligt nog är detta inte längre en mall utan en funktion - måste därför ligga i en.cpp-fil. Bild 204 22
Partiell explicit specialisering Ibland vill man ange en explicit specialisering av en hel klass, t.ex. för en viss familj av typer. Detta kan också göras med explicit (ev. partiell) specialisering. Obs! att alla klassmedlemmar i specialiseringen måste då definieras, även de som är exakt som i den generella mallen. Exempel (endast syntaktiskt, brukar inte skötas så): säg att vi vill att Vector<> som instansierats med pekartyper skall ta bort inte bara sina element (pekarna) utan även de utpekade objekten: template <class TYP> // Explicit partiell specialisering av Vector<> class Vector<TYP *>{ int int, cap; TYP **arr; ~Vector(); ; template <class TYP> // Motsvarande på andra ställen vid borttag Vector<TYP *>::~Vector(){ for(int i=0; i<siz; i++) delete arr[i]; delete [] arr; Bild 205 Förklaring till Stroustrups användning av void * Ett problem med föregående exempel är att klassmallen kommer att instansieras på nytt för varje pekartyp som Vector<> instansieras med. Detta fungerar, men ledar till onödigt stora objektkoder med kanske många upprepningar av exakt likadan maskinkod. Just för pekartyper kan detta lösas genom att göra en explicit specialisering för void * och sedan skapa mallen för en subklass - parametriserad med pekartypen: template<> class Vector<void *>{ int siz, cap; void **arr; void *& operator[](int index) { return arr[index]; ; template <class TYP> class Vector<TYP *> : private Vector<void *>{ typedef Vector<void *> Base; TYP *& operator[](int index) { return Base::operator[](index); ; Bild 206 23
Anpassning av återanvändbara klasser Säg att vi vill skapa en mall för en sorterad datastruktur för godtycklig elementtyp. Det innebär som vanligt att tillämpningsprogram måste kunna ange hur elementen skall jämföras. Det finns tre tekniker för detta: 1. Imperativ (?) lösning: tillämpningsprogrammet installerar en pekare till en jämförelsefunktion i datastrukturobjektet. Används i C samt ibland i Java (komparator-objekt). Innebär (onödig) flexibilitet under exekvering, med tillhörande ineffektivitet. 2. Objektorienterad lösning: elementtypen måste vara av en subklass till en basklass som har deklarerat en virtuell jämförelsefunktion. Subklassen definierar sin egen jämförelsefunktion som kan anropas av datastrukturen p.g.a. dynamisk bindning. Används i Java (compareto-metoden i Comparable-klasser). Innebär också run-time-flexibilitet med tillhörande kostnad 3. Generisk lösning: jämförelseoperationen anges som mallargument. Detta innebär att jämförelseoperationen kompileras in i koden för den aktuella instansen av datastrukturen. Detta ger flexibilitet vid utformning av mallen, men bestäms vid kompilering av tillämpningsprogrammet och kostar inget vid exekveringen. Bild 207 Anpassning genom mallparametrar Exempel på jämförelseoperation som mallargument: template <class TYP, class BEFORE> class Sorted{ int siz, cap; TYP *arr; BEFORE bef; Sorted(): siz(0), cap(10), arr[new TYP[cap]){ void insert(const TYP& value); ; template<class TYP, class BEFORE> void Sorted<TYP, BEFORE>::insert(const TYP& value) { for(int i = siz; i > 0 && bef(value, arr[i-1]); i--) arr[i] = arr[i-1]; arr[i] = value; // Anta för enkelhetens skull att utrymme finns ++siz; Bild 208 24
Anpassning genom mallparametrar, forts. Operationen BEFORE kan göras som en funktionsobjektsklass: struct Yngre { bool operator()(const Person& p1, const Person& p2){ return p1.age < p2.age; ; int main(){ Sorted<Person, Yngre> pers_lista; Den genererade klassen Sorted<Person, Yngre> kommer att se ut som om den skapades av följande klassdefinition: class Sorted{ int siz, cap; Person *arr; Yngre bef; Sorted(): siz(0), cap(10), arr[new Person[cap]){ void insert(const Person& value); ; void Sorted<Person, Yngre>::insert(const Person& value) { for(int i = siz; i > 0 && bef(value, arr[i-1]); i--) arr[i] = arr[i-1]; arr[i] = value; ++siz; Bild 209 Funktion som jämförelse Man kan givetvis förbereda Sorted<> till att använda en funktion istället för ett funktionsobjekt som jämförelsekriterium. Obs dock att olika booleska funktioner har samma typ, som mallargument kan man ange att Sorted<> ska ha en funktionspekare men inte vilken funktion som ska installeras - detta måste göras genom t.ex. ett argument till konstruktorn: template <class TYP, class BEFORE> class Sorted{ int siz, cap; TYP *arr; BEFORE bef; Sorted(BEFORE b): siz(0), cap(10), arr(new TYP[cap]), bef(b){ void insert(const TYP& value); ; template<class TYP, class BEFORE> void Sorted<TYP, BEFORE>::insert(const TYP& value) { for(int i = siz; i > 0 && bef(value, arr[i-1]); i--) arr[i] = arr[i-1]; arr[i] = value; ++siz; Bild 210 25
Funktion som jämförelse, forts. Nu kan operationen BEFORE kan göras som en funktion, t.ex. bool yngre(const Person& p1, const Person& p2) { return p1.age < p2.age; typedef bool(*bef)(const Person&, const Person&); int main(){ Sorted<Person, BEF> pers_lista(yngre); eller som tidigare med funktionsobjekt: Sorted<Person, Yngre> pers_lista(yngre()); Det finns flera föredelar med att använda funktionsobjekt här och inte funktionen: en funktion kan inte kompileras in, det som läggs in i den genererade klassen är en funktionspekare. När kompilatorn ser en funktionspekare kan den inte vara säker på att pekaren inte förändras mellan anropen för att peka ut en annan funktion, den kan alltså inte optimera användningen av funktionen t.ex. genom inlining. Med ett funktionsobjekt däremot ser kompilatorn ett objekt vars medlemsfunktion används, det kan inte bli ett annat objekt eller en annan funktion och användningen kan därför optimeras alla booleska funktioner med två const-referenser till Person-objekt har samma typ. Det gör att två Sorted<Person, BEF> med olika jämförelsefunktioner har samma typ och kan t.ex. tilldelas till varandra eller jämföras, vilket inte borde vara fallet Bild 211 Default-argument för mallar Liksom för funktionsargument kan man ange default- värde för klassmallargument (dock inte för funktionsmallar): template<class TYP> struct Less{ bool operator()(const TYP& x, const TYP& y){ return x < y; ; template <class TYP, class BEFORE = Less<TYP> > class Sorted{ int siz, cap; TYP *arr; BEFORE bef; Sorted(BEFORE b=less<typ>()): siz(0), cap(10), arr[new TYP[cap]), bef(b){ void insert(const TYP& value); Om operator< är definierad för elementtypen och man vill ha stigande sorteringsordning så behöver inte jämförelseoperationen anges vid deklaration: Sorted<Person> pers_lista; Bild 212 26
Exempel från std: basic_string<> I många situationer kan mallar göras mer generella genom att ange olika egenskaper för t.ex. elementtypen som parametrar till mallen. Sådana egenskaper brukar då samlas som statiska medlemsfunktioner i en s.k. traits-class. Exempel: typen string är egentligen en instansiering av mallen basic_string<> vars parametrar är teckentypen, dess egenskaper ( traits ) och minneshantering: namespace std{ template <class chart = char, // Teckentypen class traits = char_traits<chart>, // Hur tecken jämförs class allocator = allocator<chart> > // Hur man allokerar class basic_string{ ; typedef basic_string<char> string; Man kan alltså lätt skapa en strängtyp där tecknen t.ex. jämförs på annat sätt än vad som är inbyggt för typen char (se nästa bild). Samtidigt kostar det mycket lite vid exekveringen (behövde man anropa en riktig funktion vid varje teckenjämförelse skulle det bli oacceptabelt ineffektivt). Bild 213 Exempel: en egen char_traits Säg att vi behöver en strängtyp där tecknens skiftläge (gemena eller versaler) inte påverkar jämförelser: #ifndef ICSTRING_H #define ICSTRING_H #include <string> #include <cctype> struct ignore_case_traits: public std::char_traits<char>{ static bool eq(const char& c1, const char& c2){ return std::toupper(c1) == std::toupper(c2); static bool lt(const char& c1, const char& c2){ return std::toupper(c1) < std::toupper(c2); static int compare(const char *s1, const char *s2, size_t n){ size_t i; for(i=0; i<n-1 &&!eq(s1[i], s2[i]); ++i) ; return s1[i]-s2[i]; ; typedef std::basic_string<char,ignore_case_traits> icstring; #endif Bild 214 27