*:85/ID200V C++ HT07 Föreläsning 5-7 Operatoröverlagring Operatoröverlagring Följande operatorer kan inte överlagras: ::..*?: sizeof typeid Alla övriga operatorer kan överlagras. Följande operatorer måste vid överlagring göras till medlemsfunktioner: tilldelningsoperatorer = += -= *= osv indexeringsoperatorn [] anropsoperatorn () medlemsåtkomst via pekare -> Övriga överlagringsbara operatorer kan göras till medlemsfunktioner eller till funktioner utanför klassen Bild 78 1
Operatoröverlagring Exempel för OH-bilderna: antag följande klass för representation av klockslag eller tidsåtgång: class Klocka{ Klocka(int m=0) : min(m){ // Default-ktor Klocka(int h, int m) : min(h*60+m){ int timme() const { return min / 60; int minuter() const { return min % 60; int totmin() const { return min; private: int min; ; Exempel som löses på föreläsningen: i en del tillämpningar (speciellt sådana som har att göra med kryptering) behöver man hantera heltal som är mycket större än vad som kan representeras med inbyggda heltalstyper. Skapa en klass vars objekt beter sig som heltal utan förtecken (unsigned) av obegränsad storlek. Vi begränsar oss till additionsoperatorer för dessa heltal. Bild 79 Operatoröverlagring forts. Vi kan definiera t.ex. följande funktioner: const Klocka operator+(const Klocka& k1, const Klocka& k2){ return Klocka(k1.totmin()+k2.totmin()); const Klocka operator+(const Klocka& k, int i){ return Klocka(k.totmin()+i); inline const Klocka operator+(int i, const Klocka& k){ return k+i; // Anropar bara ovanstående operator+ int operator-(const Klocka& k1, const Klocka& k2){ return k1.totmin()-k2.totmin(); Obs att det är biblioteksskaparens uppgift att se till de vanliga reglerna för operatorerna gäller, t.ex. att additionen är kommutativ (kan man göra kl + 3 så borde man kunna göra 3 + kl) Att returtypen för de operatorer som returnerar Klocka-objekt deklareras som const Klocka beror på att man vill se det returnerade objektet som ett värde, inte som ett lvalue (tilldelningsbart objekt). Bild 80 2
Operatoröverlagring - medlem eller fri funktion Operatorer som görs som medlemsfunktioner har som sin vänstra operand objektet självt och deklareras med en operand mindre. De har tillgång till objektets privata data. Operatorer vilkas vänstra operand är av en annan typ kan inte göras så. class Klocka { int min; int totmin() const{ return tot_minuter; const Klocka operator+(const Klocka& k) const; ; const Klocka Klocka::operator+(const Klocka& k) const { return Klocka(min + k.min); Operatorer som görs utanför klassen deklareras med alla sina operander som argument. De har inte tillgång till objektets privata data (kan lösas med frienddeklarationer): const Klocka operator+(int min, const Klocka& k){ return Klocka(min + k.totmin()); Bild 81 Relationsoperatorer Det vore ju bra om två klockslag kunde jämföras - detta kräver definition av jämförelseoperatorer med Klocka-operander. Obs att ingen operator genereras automatiskt (utom tilldelningsoperatorn): att man definierat operator== betyder inte att operator!= är definierad (se dock nästa bild). bool operator==(const Klocka& k1, const Klocka& k2){ return k1.timme()==k2.timme()&&k1.minuter()==k2.minuter(); inline bool operator!=(const Klocka& k1, const Klocka& k2){ return!(k1 == k2); Obs att det är en god vana att använda redan definierade operatorer vid definition av en ny operator (om det går). På detta sätt försäkrar man sig om att operatorernas semantik är konsekvent och underlättar ev. modifieringar. Vidare: bool operator<(const Klocka& k1, const Klocka& k2){ return k1.timme()<k2.timme() k1.timme() == k2.timme() && k1.minuter()<k2.minuter(); bool operator<=(const Klocka& k1, const Klocka& k2){ return k1 < k2 k1 == k2; Bild 82 Och så vidare 3
To the rescue - mallar från std I standardbiblioteket finns mallar för relationsoperatorer definierade m.h.a. operator== och operator< Om dessa definitioner duger behöver man alltså bara definiera dessa två operatorer och inkludera mallarna. Tillgängliga genom #include <utility>, finns i namnrymden std::rel_ops template <class T> inline bool operator!=(const T& x, const T& y) { return!(x == y); template <class T> inline bool operator>(const T& x, const T& y) { return y < x; template <class T> inline bool operator<=(const T& x, const T& y) { return!(y < x); template <class T> inline bool operator>=(const T& x, const T& y) { return!(x < y); Bild 83 Konverteringsoperationer Vad skulle kompilatorn säga om följande sats (med endast de hittills visade operatorerna och om kl är ett objekt av typen Klocka): if (kl < 50) Jo, det går hur bra (eller dåligt, beroende på vad man vill) som helst. Kompilatorn försöker hitta en operator<(klocka&, int), hittar ingen sådan men hittar en operator<(const Klocka&, const Klocka&) och dessutom en Klocka::Klocka(int), d.v.s. den kan konvertera en int till en Klocka (skapa temporärt objekt) och utföra operationen. Vill man förbjuda detta kan man deklarera Klocka konstruktorn som explicit. Går det då att göra tvärtom (använda Klocka där en int förväntas), t.ex.: int m = kl; Nej, kompilatorn vet inte hur man konverterar Klocka till int. Om detta är önskvärt (mycket tveksamt, se det bara som syntaktiskt exempel) kan man definiera en konverteringsoperator till int i klassen Klocka (se nästa bild). Bild 84 4
Konverteringsoperatorer En konstruktor kan användas för att konvertera från en annan typ till denna klass (explicit eller implicit), t.ex. från int till Klocka Om man vill ange hur ett objekt av klassen kan konverteras (explicit eller implicit) till en annan typ kan man utrusta klassen med en konverteringsoperator, vars namn är typen man skall konvertera till och som deklareras utan returtyp: class Klocka{ int min; explicit Klocka(int m=0):min(m){ operator int() { return min; ; Detta gör att ett Klocka-objekt accepteras där en int förväntas. Explicit konvertering till int av Klocka kl görs med int(kl). Explicit konvertering av int i till Klocka kan fortfarande göras med Klocka(i). Implicit konvertering av int till Klocka är nu förbjuden (kompileringsfel). Obs att man inte kan ha båda: om konstruktorn som tar en int inte vore explicit får man tvetydighet vid behov till implicit konvertering, båda konverteringarna duger lika bra! Bild 85 Uppdateringsoperatorer För typer där både tilldelningsoperatorn och vanliga binära operatorer (t.ex. binär +, binär - o.s.v.) är definierade förväntar sig användare att även uppdateringsoperatorer (+=, -= o.s.v.) kan användas. Dessa borde alltså också definieras. Vår klass Klocka har ju den automatiskt genererade tilldelningsoperatorn och så har vi definierat operatorerna operator+ med en annan Klocka och med int samt operator- med en annan Klocka. Alltså borde vi definiera motsvarande += resp. -= Sådana operatorer måste göras som medlemsfunktioner. Eftersom de ändrar det egna objektet kan de inte deklareras som const. Eftersom de skall returnera det tilldelade värdet (de borde ju bete sig som andra likadana operatorer) så brukar man returnera en referens till objektet självt, men som en const-referens så att returen inte skall vara tilldelningsbar. För att återanvända redan implementerade operationer brukar man definiera uppdateringsoperatorn först och sedan använda den i definitionen av motsvarande binära operator (se nästa bild). Bild 86 5
Exempel på uppdateringsoperatorer class Klocka{ int min; const Klocka& operator+=(const Klocka&); const Klocka operator+(const Klocka&) const; ; const Klocka& Klocka::operator+=(const Klocka& other){ min += other.min; return *this; const Klocka Klocka::operator+(const Klocka& other) const{ Klocka resultat = *this; resultat += other; return resultat; Bild 87 Konstigheter med ++ och -- För typer där det finns tilldelning samt addition och subtraktion förväntar sig användare att det även finns ++ och --. Inkrementeringsoperatorn ++ och dekrementeringsoperatorn -- finns ju i två varianter: prefix och postfix. I dessa två varianter har de olika semantik men samma signatur. På något sätt måste man ange i deklarationen av dem vilken som är prefix och vilken som är postfix. Detta görs genom att postfix-operatorerna utrustas i deklarationen (och definitionen) med ett extra dummy int-argument som varken används eller ens namnges (!). Prefix-operatorerna borde returnera objektet självt som en const-referens. Postfix-operatorerna kan inte göra det - de skall ju returnera värdet innan förändringen. De bör alltså skapa ett nytt objekt och returnera det (alltså objektet, inte en referens). class Klocka{ int min; const Klocka& operator++() { // Prefix return *this += 1; const Klocka operator++(int) { Klocka tmp(*this); *this += 1; return tmp; // Postfix Bild 88 6
Överlagring av operator() Om man deklarerar anropsoperatorn operator() i en klass så innebär det bara att man kan anropa denna operator genom att skriva parenteserna med eventuella argumentvärden efter ett objektnamn. Denna operator är den enda där antalet argument inte är bestämt av språkdefinitionen. class Exempel { string str; Exempel(string s):str(s) { void operator()(int x, string mer){ cout << str << mer << x << endl; ; int main(){ Exempel ex( Karl ); ex(16, Gustav ); // Anropar operator() Bild 89 operator(), användning - funktionsobjekt Den viktigaste användningen av överlagrad anropsoperator är för skapande av s.k. funktionsobjekt (funktorer), d.v.s. objekt som omsluter en funktion och kan användas som en funktion för skickas till andra funktioner som argument (istället för funktionspekare). Exempel: i standardbibliotekets headerfil <algorithms> finns bl.a. följande funktionsmall motsvarande våra doforall-funktioner från moment *:58 (obs! förenklad här!): template <class Iter, class Oper> void for_each(iter first, Iter last, Oper doit) { for ( ; first!= last; ++first) doit(*first); Exempel på användning: void show_one(int value) { cout << value; int main(){ vector<int> vec; // Fyll vec med värden på nå t sätt for_each(vec.begin(), vec.end(), show_one); Endast orinentering, återkommer vid genomgång av STL Bild 90 7
Funktionsobjekt, exempel, forts. Antag att vi ofta behöver öka alla element i vektorn med samma värde, men detta värde är olika vid olika tillfällen. En funktion som skulle kunna skickas till for_each för att göra detta behöver ta två argument: en referens till elementet som ska ökas och värdet som ska adderas men for_each anropar funktionen med endast elementet Genom att istället för en funktionspekare skicka ett objekt av en struct som omsluter funktionen kan detta objekt även innehålla värdet som funktionen ska använda - vi skickar en funktion med en liten omgivning: struct Add{ int talet; Add(int tal):talet(tal){ void operator()(int& elem){ elem += talet; ; int main(){ vector<int> vec; // Fyll vec med värden for_each(vec.begin(), vec.end(), Add(5)); I for_each-anropet skapas ett temporärt objekt av Add och värdet 5 lagras i objektet, detta objekt används av for_each som en funktion. Bild 91 operator(), användning - indexering med flera index Eftersom operator() är den enda där antalet argument inte är bestämt av språkdefinitionen används den ofta även där indexeringsoperatorn egentligen borde användas, men där man vill ha flera index. Exempel: class Matrix { int rows, cols; int *data; Matrix(int r, int c): rows(r), cols(c), data(new int[rows * cols]){ ; int& operator()(int r, int c){ return data[r * cols + c]; int main(){ Matrix mat(10, 30); mat(3, 5) = 177; Bild 92 8
Överlagring av operator[] Indexeringsoperatorn operator[] brukar överlagras i klasser som fungerar som en datasamling, den brukar ge åtkomst till ett element i datasamlingen identifierat med ett värde (ofta ett heltalsindex, men det är inte nödvändigt). Den ska ha ett argument. I icke-const-objekt borde den returnera ett lvalue (alltså en referens) så att den ska kunna användas både för avläsning av elementvärdet och för tilldelning till elementet. Brukar även överlagras med en const-variant som returnerar ett värde. class Text{ int siz; char *cptr; char& operator[](int index){ return cptr[index]; char operator[] (int index) const { return cptr[index]; ; Bild 93 Exempel: matris med vanliga indexeringsoperatorer Det vanliga sättet att indexera i en matris i C-liknande språk är med dubbel indexering: mat[3][5] = 177; Här är det alltså resultatet av den första indexeringen som indexeras i sin tur. För att åstadkomma detta får vi införa en extra klass eller struct som returneras som resultat av den första indexeringen: class Matrix { int rows, cols; int *data; struct Col{ Matrix& mx; int r; Col(Matrix& m, int rr):mx(m), r(rr){ int& operator[](int c) { return mx.data[r*mx.cols+c]; ; Här har vi ett exempel på en nästlad struct. Obs! skillnad mot Javas inre klasser! Objekt av en nästlad struct eller klass i C++ ligger inte inom sitt omgivande objekt. Matrix(int r, int c): rows(r), cols(c), data(new int[rows * cols]){ Col operator[](int r){ return Col(*this, r); ; Bild 94 9
operator[], skilja på avläsning och tilldelning Ibland vill man använda använda operator[] för åtkomst, men måste göra operationerna avläsning resp. tilldelning på olika sätt. Man kan då från operator[] returnera ett temporärt objekt av en privat inre klass med en konverterings- och en tilldelningsoperator. Exempel: en klass som beter sig som en bitvektor med indexering till ensklida bitar(!) class Bitvector{ unsigned long bits; struct Bref{ unsigned long& b; int pos; Bref(unsigned long& bb, int p):b(bb), pos(p){ operator bool() const { return b & (1ul << pos); bool operator=(const bool v) { if (v) b =1ul<<pos; else b&=~(1ul<<pos); ; Bitvector():bits(0){ Bref operator[](int pos){return Bref(bits, pos); ; Bild 95 Överlagring av operator-> operator-> har lite speciellt beteende: den skall returnera en pekare eller en referens till ett objekt av en klass som också överlagrar denna operator. Om returtypen är en pekare så avrefereras pekaren och åtkomst görs till den angivna medlemmen i det utpekade objektet (som om klassen som överlagrar operatorn själv var en pekare). Om returtypen är en referens till ett objekt av en klass som också överlagrar operator-> så anropas denna operator och mekanismen tillämpas rekursivt tills en pekare returneras, varefter pekaren avrefereras och medlemsåtkomsten görs. Överlagrad operator-> måste vara implementerad som medlem i klassen. Med hjälp av överlagring av operator-> (ofta i kombination med den unära operator*) kan man skapa klasser som beter sig som pekare men kan utföra kontroller, minneshantering osv (s.k. smarta pekare). Behållarklassernas iteratorer utgör ett exempel på sådana klasser. Bild 96 10
Pekarklasser Vid byggande av bibliotek av klasser där det är meningen att tillämpningar skall skapa objekten dynamiskt kan man underlätta hanteringen av objektavallokering genom att även skapa klasser vilkas objekt beter sig som pekare men som kontrollerar de utpekade objektens liv genom att när det är dags avallokera dem. Vad som är lämplig tidpunkt för avallokering varierar med situationen varför det har varit svårt att skapa bibliotekskomponenter för detta. På nästa bild finns det en skiss på en sådan klass för hantering av objekt av en klass Person. Klassen Ptr låtsas vara en pekare, den skulle kunna användas i en tillämpning enligt följande: void funk(){ Ptr p=new Person( Jozef ); cout << p->getname(); // åtkomst till medlem i utpekat objekt Person annan(*p); // avreferering ger det utpekade objektet // funktionen slut, p städas bort och i sin destruktor tar bort utpekat objekt Bild 97 Exempel: klass som simulerar pekare class Person{; class Ptr{ Person *ptr; // info om utpekade objektets status Ptr(Person *p): ptr(p){ /* uppdatera status */ Ptr(Ptr& other):ptr(other.ptr){/* uppdatera status */ Ptr& operator=(const Ptr& other){ /* updatera status */ ~PersPtr(){ if (/* skall bort */) delete ptr; ; Person& operator*() { return *ptr; Person* operator->() { return ptr; bool operator==(const Ptr& other) const; bool operator!=(const Ptr& other) const; Bild 98 11
Referensräkning En lösning som duger i många situationer är pekarklasser som håller reda på hur många pekarobjekt som pekar ut samma objekt och som städer bort objektet när det sista pekarobjektet försvinner: class Person; class Ptr { Person *ptr; int *count; // Räknare som pekarobjekten till samma obj. delar på Ptr(Person *pek=0); Ptr(const Ptr& other); ~Ptr(); Ptr& operator=(const Ptr& other); Person& operator*() const { return *ptr; Person* operator->() const { return ptr; bool operator==(const Ptr& other) const; bool operator!=(const Ptr& other) const; ; Bild 99 Referensräkning - implementering av medlemsfunktioner Ptr::Ptr(Person *pek) : ptr(pek), count((pek)? new int(1) : 0){ Ptr::Ptr(const Ptr& other) : ptr(other.ptr), count(other.count){ if (ptr) (*count)++; Ptr::~Ptr(){ if (ptr && --*count == 0){ delete count; delete ptr; bool Ptr::operator==(const Ptr& other) const{ return ptr == other.ptr; bool Ptr::operator!=(const Ptr& other) const{ return ptr!= other.ptr; Bild 100 12
Referensräkning - implementering av tilldelning Ptr& Ptr::operator=(const Ptr& other){ if (ptr!= other.ptr){ if (ptr && --*count == 0){ delete count; delete ptr; ptr = other.ptr; count = other.count; if (ptr) ++*count; return *this; Användning av denna pekarklass förutsätter att användaren har disciplinen att inte använda vanliga pekare - vår Ptr kan ju inte veta hur många vanliga pekare som pekar ut de utpekade Person-objekten. Detta kan underlättas genom att vid objektskapande påtvinga användning av en funktion som returnerar ett Ptr-objekt. Bild 101 Person-klassen för referensräkning Genom att göra konstruktorn privat och skapa en publik statisk medlemsfunktion som skapar ett objekt dynamiskt och returnerar ett Ptr-objekt kan man se till att tillämpningar inte kan skapa objekt på annat sätt: class Person{ string namn; int nummer; Person(string na, int nu):namn(na), nummer(nu){ static Ptr create(string na, int nu); string get_namn() const { return namn; int get_nummer() const { return nummer; ; // Person Ptr Person::create(string na, int nu){ return Ptr(new Person(na, nu)); Objektskapande funktioner brukar kallas fabriksfunktioner (factory functions) Tekniken med privat konstruktor och en objektskapande statisk medlemsfunktion (eller friend-funktion) brukar kallas the named constructor idiom. Bild 102 13
Person-klassen för referensräkning, forts. Om man verkligen vill försäkra sig om att Person-objekten bara skapas på heapen och hanteras genom Ptr-objekt bör man även förbjuda kopiering av Person-objekt och adresstagning för dem: class Person{ // som tidigare Person(const Person& other); Person *operator&(); const Person *operator&() const; // som tidigare ; // Person Detta brukar göras genom att man deklarerar copy-konstruktorn och adresstagningsoperatorer (för icke-const-resp. const-objekt) som privata och utan definition (det senare dels för att definitionen är onödig, dels för att det skyddar mot att man använder dessa operationer själv i klassen Person - ger då länkningsfel). Eftersom detta kan behöva göras för många klasser brukar man definiera ett preprocessormacro för sådana deklarationer. Bild 103 Standardbibliotekets auto_ptr<> Standardbiblioteket tillhandahåller i <memory> mallen för en pekarklass kallad auto_ptr<>. Semantiken för denna klass är att ett auto_ptr<>-objekt äger det utpekade objektet och tar bort det i sin destruktor. Vid kopiering och tilldelning överförs ägarskapet till det nya auto_ptr<> och det gamla sätts att peka på NULL. Det är alltså endast det senast tilldelade eller konstruerade auto_ptr<> som pekar ut objektet. Detta gäller även vid värdeöverföring som argument eller vid värderetur från funktioner. auto_ptr<> kan inte användas i situationer där flera pekare kan tänkas peka ut samma objekt, t.ex. i behållare. auto_ptr<>s användningsområde är för situationer då dynamiska objekt skapas, används och skall städas bort i funktioner som kan avbrytas av undantag. Bild 104 14