Operator Überladung
nach Thinking in C++ Band 1 von Bruce Eckel (Sehr gute Einführung in C++)
und Large Scale C++ Software Design von John Lakos (Betrachtungen über Compile- und Linkabhängigkeiten in großen C++ Projekten)
- Operator Überladung stellt eine zusätzliche Art dar, Funktionsaufrufe zu erzeugen
- diese zusätzliche Möglichkeit wird gern als "syntatic sugar" bezeichnet
int main(){ int main(){ pub_Int a, b, c, d, e , f ; pub_Int a, b, c, d, e , f ; a +=1; a.add(1); c = a+b; c = pub_Int::plus(a,b); f = a*b*c + b*c*d + c*d*e; f= pub_Int::plus( pub_Int::plus( pub_Int::mult( pub_Int::mult(a,b), c), pub_Int::mult( pub_Int::mult(b,c), d) ), pub_Int::mult( pub_Int::mult(c,d), e) ); std::cout << f << std::endl; pub_Int::print( cout, f ) << std::endl;
- Lesbarkeit und Codierbarkeit wird durch diese Technik deutlich erhöht, da die Operanden nicht nach dem Operator ( pub_Int::plus (a,b) ) stehen
- Operator Überladung sollte nur dort angewandt werden, wo es den Code leichter codierbar und insbesondere lesbarer macht
Einschränkungen
- nur in Ausdrücken mit eigenen Datentypen kann man Operator überladen
man kann den= << =
Operator nicht so überladen, sodass= 1.414 << 2 =
vom Compiler angenommen wird - es können keine neuen Operatoren (** für pow) definiert werden, die keine Bedeutung in C besitzen
- da man hierzu einerseits die Präzedenz der neuen Operatoren definieren müsste und andererseits neue Parsingprobleme (vgl. **) einführt
- der Memberfunktionsoperator Operator "." und der Operator ".*" , um Zeiger auf Members zu dereferenzieren, können nicht überladen werden
struct X { int X::*pm; int y; } s; s.pm = &X::y; s.pm gives you the pointer to member. s.*pm // gives you the member pointed at.
- die Präzedenz der Operatoren kann nicht verändert werden
- die Anzahl der Argumente eines Operators steht fest
Syntax
- um den Operator
@
zu überladen, wird eine Funktionoperator@
erklärt - die Anzahl der Argumente hängt davon ab, ob es eine unärer oder binärer Operator ist
- wenn der Operator als Memberfunktion erklärt ist, wird implizit das aufrufende Objekt als Argument übergeben
//OperatorOverloadingSyntax.cpp #include <iostream> using namespace std; class Integer { int i; public: Integer(int ii) : i(ii) {} const Integer operator+(const Integer& rv) const { cout << "operator+" << endl; return Integer(i + rv.i); } Integer& operator+=(const Integer& rv) { cout << "operator+=" << endl; i += rv.i; return *this; } }; int main() { cout << "built-in types:" << endl; int i = 1, j = 2, k = 3; k += i + j; cout << "user-defined types:" << endl; Integer ii(1), jj(2), kk(3); kk += ii + jj; }
- Vorsicht:
const Integer operator+(const Integer& rv) const
gibt eine sogenannten "right value" zurück; dieser Typ zeichnet sich dadurch insbesondere aus, dass er keine Adresse hat und so nur auf der rechten Seite von Zuweisungen stehen darf
a+b=c+d ist daher nicht möglich- hingegen gibt
Integer& operator+=(const Integer& rv)
eine "left value" zurück, der auf beiden Seiten einer Zuweisung stehen kann
Unäre Operatoren
- die Increment- und Dekrementoperatoren sind ein bisschen tricky:
- für die Inkrementoperator (Decrementoperator) gibt es zwei Formen, die Prefix ++a (--a) und die Postfix a++ (a--) Form
- die Prefix Form inkrementiert (dekrementiert) erst, bevor sie den Wert zurückgibt; die Postfix Form gibt den Wert unverändert zurück und inkrementiert (dekrementiert) erst danach
- ++a stößt den operator++(a) und a++ hingen den operator++(a, int) an
- wenn möglich, sollte der Prefixoperator verwendet werden, da dieser ohne ein temporäres Objekt auskommt
for (int i =0; i < a; ++i ) for (int i= 0; i < a; i++ )
Binäre Operatoren:
Ene Sonderstellung geniess der Zuweisungsoperator.
- der
operator=
muss als einziger eine Memberfunktion sein - bei der Implementierung des Operators sollte man keine Selbstzuweiung zulassen
Fred& operator= (const Fred& f){ delete p_; p_ = new Wilma(*f.p_); return *this; }
- falls f und this identisch sind, werden beide Objekte this->p_ und f.p_ durch
delete p_
gelöscht, aber in der nächsten Zeile wird das nicht mehr existierende Objekt f.p_ referenziert
Argument- und Returnwertbetrachtungen nicht nur für Operatoren
- Argument: konstante Referenz, falls nur lesend darauf zugegriffen wird
- Memberfunktion: const, falls keine Veränderung an dem Objekt (Operanden) geschieht
- Returnwert:
- "by Value" ein konstantes, temporäres Objekt wird erzeugt, ein sogenannter "rvalue", der nur zugewiesen werden kann
die Deklaration von Integer::opertor+ unterbindet a+b=c+d - "by Reference" durch die Rückgabe als "lvalue" sind Operatorketten wie a=b=c möglich
falls der Returnwert modifzierbar sein soll, darf die Referenz nicht const sein (a=b).foo()
- "by Value" ein konstantes, temporäres Objekt wird erzeugt, ein sogenannter "rvalue", der nur zugewiesen werden kann
- Logische Operatoren sollten ein bool Objekt zurückliefern
- Returnwertoptimierung:
return Integer(left.i + right.i)
- hier greift die Returnwertoptimierung, sodass nur ein Konstruktoraufruf benötigt wird
- der Compiler sieht, dass es hier nur um einen Returnwert geht
=Integer tmp(left.i + right.i ); return tmp; =
- ein Konstrukt-, ein Kopykonstruktor- und ein Destruktoraufruf wird für tmp benötigt
Member- versus Nichtmember Operator
- die folgende Zusammenstellung geht auf eine Rob Murray, C++ Strategies & Tactics zurück:
Operator | Empfohlener Gebrauch |
---|---|
alle unären Operatoren | Member |
= () [] -> ->* | müssen Member sein |
+= -= /= *= ^= &= %= >>= <<= | Member |
die restlichen binären Operatoren | nicht Member |
- Freie Funktion:
- unterstützte "user-defined" Konvertierung des am weitestesten Links stehenden Arguments (implizite Argument bei Memberoperatoren)
- Symmetrie der binären Operatoren ( z.B.:.: == < + )
- Memberoperator:
- unterbinde "user-defined" Konvertierung des am weitestesten Links stehenden Arguments
- Operator modifiziert den Operand ( z.B.: = += *= ++ )
- C++ Standard schreibt es vor ( z.B.: () [] -> )
Beispiele
user-defined Konvertierung oder auch Wieso sollte der += operator Member sein ?
class pub_String{ public: pub_String( const char* str); const pub_String operator+ ( const& pub_String right) const; friend pub_String& operator+=( pub_String& left, const pub_String& right); } void f(){ pub_String s("foo"), t(""); int i; t= s+ "bar"; // ok t= "bar" + s; //error i= s == "bar"; // ok i= "bar" == s; // error } void g(){ pub_String a("tar"); const char* b= "foo"; pub_String c("bar"); b += c; // keine Wirkung auf b; a += b += c; // keine Wirkung auf b, aber auf a }
- zu f: das Bereitstellen des Konvertierungsoperators von const char* nach pub_String bricht die Symmetrie
- zu g:
- b += c; besitzt keine Wirkung, da c hier nicht *b, sondern dem temporären Objekt pub_String("foo") zugewiesen wird
- a += b += c; verändert nur a, da das temporäre Objekt pub_String("foo") an a gebunden werden kann; b bleibt aber dennoch unverändert
Symmetrie oder auch Wieso sollte der + operator frei sein ?
class Number { int i; public: Number(int ii = 0) : i(ii) {} const Number operator+(const Number& n) const { return Number(i + n.i); } friend const Number operator-(const Number&, const Number&); }; const Number operator-(const Number& n1, const Number& n2) { return Number(n1.i - n2.i); } int main() { Number a(47), b(11); a + b; // OK a + 1; // 2nd arg converted to Number //! 1 + a; // Wrong! 1st arg not of type Number a - b; // OK a - 1; // 2nd arg converted to Number 1 - a; // 1st arg converted to Number }
- durch den Konvertierungskonstruktor
Number(int ii = 0)
können die Operatoren + und - für int verwendet werden 1 + a
kann vom Compiler nicht verwertet werden, da das linke Argument vom Typ Number sein muß
Automatische Typkonvertierung
Konstruktor
- ein typisches Beispiel ist
Number(int ii = 0 )
- unterbinden der impliziten Konstruktorkonvertierung:
- durch
explicit Number(int ii = 0 )
unterbindet man die implizite Konvertierung von 1 zu Number(1) im Ausdruck1 + a
- obige main Funktion müsste umgeschrieben werden:
- durch
int main() { Number a(47), b(11); a + b; // OK a + Number(1); // 2nd arg converted to Number //! 1 + a; // Wrong! 1st arg not of type Number a - b; // OK a - Number(1); // 2nd arg converted to Number Number(1) - a; // 1st arg converted to Number }
Operator
- um den aktuellen Typ zu einem anderen Zieltyp umzuwandeln, bietet sich die Konvertierung durch einen Operator an
- durch die Memberfunktion
operator NewType()
in der KlasseOldType
erkläre ich, dass die implizite KonvertierungOldType
zuNewType
unterstützt wird - Beispiel:
struct Three {}; struct Four { public: operator Three() const { return Three; } }; void g(Three) {} int main() { Four four; g(four); }
Unterschied
struct A{ A( const B& ); // konverierte B zu A operator B() const; // konvertiere ein A zu B };
Während beim Konvertierungskonstruktor die Zielklasse die Umwandlung bestimmt, stösst bei der Operatorkonvertierung die "Ursprungsklasse" die Konvertierung an.
Besondere Operatoren:
Zuweisungsoperator (operator= ):
MyType b; MyType a = b; // 1 a = b; // 2
- in 1 wird aus dem bestehenden Objekt b ein neues Objekt a konstruiert
- der Kopykonstruktor
MyType( const MyType& )
wird vom Compiler verwendet
- der Kopykonstruktor
- dahingegen werden in 2 einem fertigen Objekt a ein anderes Objekt b zugewiesen
operator= ( const MyType& )
schlägt zu
- falls der Zugweisungsoperator in einer Klasse nicht definiert wird, erzeugt der Compiler einen entsprechend als Memberfunktion
class SimpleValues{ int a; double b; public: SimpleValues& operator=(const Value& rv) { a = rv.a; b = rv.b; return *this; }
- enthält die Klasse hingegen Pointer, muß wie beim Kopykonstruktor die Selbstzuweisung unterbunden werden
class NotSoSimpleValues{ pointer* p; int a; NoSoSimpleValues& operator=(const NotSoSimpleValues& rv) { // Check for self-assignment: if(&rv != this) { p = new NotSoSimpleValue(); a= rv.a; } return *this; }
class CoreDump{ pointer p; int a; public: void setA(int b){ a=b;} CoreDump& operator=(const CoreDump& rv){ p = rv.p; a= rv.a; return *this; } .... } int main(){ CoreDump* a= new CoreDump; CoreDump* b= new CoreDump; *a= *b; del b; // => del a; a->setA(10); // crash }
- daher soll bei Klassen mit Pointern der Zuweisungsoperator definiert oder unterbunden werden, da der vom Compiler generierte elementweise - ohne Test auf Selbstzuweisung - kopiert
new and delete Operator
- beim Erzeugen eines neuen Objekts geschehen zwei Dinge
- mittels des Operators
operator new()
wird Speicher allokiert - der Konstruktor des Objekts wird aufgerufen
- mittels des Operators
- beim Löschen des Objekts fallen entsprechend wieder zwei Operationen an:
- der Destruktor
operator delete()
wird aufgerufen - der Speicher wird wieder freigegeben
- der Destruktor
- um vollkommene Kontrolle über den Speicher zu erhalten, können die beide Operatoren sowohl auf Klassenebene wie auch global überladen werden
- Gründe fürs Überladen:
- Performancegewinn: Oftmaliges Erzeugen/Löschen von Objekte des gleichen Typs
- Heap Fragmentation vermeiden: Freier Speicher ist zwar noch verfügbar, liegt aber nicht mehr zusammenhängend vor
- Echtzeitanforderung: Speicheranforderung soll in konstanter Zeit geschehen
Global:
Syntax
void* operator new(size_t)
void operator delete(void* )
- der new operator sollte den entsprechenden Speicher zurückgeben oder eine
std::bad_alloc
Ausnahme auslösen - beide Operatoren arbeiten mit void Pointern, da der new Operator rohen Speicher an der Konstruktor gibt und der delete Operator rohen Speicher vom Destruktor bekommt
- der new operator sollte den entsprechenden Speicher zurückgeben oder eine
Beispiel
#include <cstdio> #include <cstdlib> using namespace std; void* operator new(size_t sz) { printf("operator new: %d Bytes\n", sz); void* m = malloc(sz); if(!m) puts("out of memory"); return m; } void operator delete(void* m) { puts("operator delete"); free(m); } class S { int i[100]; public: S() { puts("S::S()"); } ~S() { puts("S::~S()"); } }; int main() { puts("creating & destroying an int"); int* p = new int(47); delete p; puts("creating & destroying an s"); S* s = new S; delete s; puts("creating & destroying S[3]"); S* sa = new S[3]; delete []sa; }
In new und delete sollen keine iostreams Objekte verwendet werden, da diese Speicher anfordern.
Lokal:
#include <cstddef> // Size_t #include <fstream> #include <iostream> #include <new> using namespace std; ofstream out("Framis.out"); class Framis { enum { sz = 10 }; char c[sz]; // To take up space, not used static unsigned char pool[]; static bool alloc_map[]; public: enum { psize = 100 }; // frami allowed Framis() { out << "Framis()\n"; } ~Framis() { out << "~Framis() ... "; } void* operator new(size_t) throw(bad_alloc); void operator delete(void*); }; unsigned char Framis::pool[psize * sizeof(Framis)]; bool Framis::alloc_map[psize] = {false}; // Size is ignored -- assume a Framis object void* Framis::operator new(size_t) throw(bad_alloc) { for(int i = 0; i < psize; i++) if(!alloc_map[i]) { out << "using block " << i << " ... "; alloc_map[i] = true; // Mark it used return pool + (i * sizeof(Framis)); } out << "out of memory" << endl; throw bad_alloc(); } void Framis::operator delete(void* m) { if(!m) return; // Check for null pointer // Assume it was created in the pool // Calculate which block number it is: unsigned long block = (unsigned long)m - (unsigned long)pool; block /= sizeof(Framis); out << "freeing block " << block << endl; // Mark it free: alloc_map[block] = false; } int main() { Framis* f[Framis::psize]; try { for(int i = 0; i < Framis::psize; i++) f[i] = new Framis; new Framis; // Out of memory } catch(bad_alloc) { cerr << "Out of memory!" << endl; } delete f[10]; f[10] = 0; // Use released memory: Framis* x = new Framis; delete x; for(int j = 0; j < Framis::psize; j++) delete f[j]; // Delete f[10] OK }
- durch das Überladen erzeugt man implizit statische Memberfunktionen
unsigned char Framis::pool[psize * sizeof(Framis)];
legt den Speicher anbool Framis::alloc_map[psize] = {false};
führt dann über diesen Buch, indem jeder unbenutzt Speicherblock auf false gesetzt wird- der sogenannten Aggregate Inizialiserungs Trick bewirkt, dass alle restlichen Elemente von
Framis::alloc[]
default Initialisiert werden, da das erste Element initialisiert wurde - im new Operator wird so nach einem unbenutzten Speicher gesucht und entweder die Adresse des ersten zu Verfügung stehen Bereichs zurückgegeben oder eine bad_alloc Exception ausgelöst, wie in der Signatur erklärt
- der delete Operator ermittelt aus der Adresse des freizugebenden Speichers desssen Index und markiert den Speicherbereich als frei
- im Gegensatz zum globen new und delete Operator dürfen in den lokalen Formen iostreams verwendet werden
- für arrays vom Typ Framis werden die globalen new und delete Operatoren verwendet
Arrays:
#include <new> // Size_t definition #include <fstream> using namespace std; ofstream trace("ArrayOperatorNew.out"); class Widget { enum { sz = 10 }; int i[sz]; public: Widget() { trace << "*"; } ~Widget() { trace << "~"; } void* operator new(size_t sz) { trace << "Widget::new: " << sz << " bytes" << endl; return ::new char[sz]; } void operator delete(void* p) { trace << "Widget::delete" << endl; ::delete []p; } void* operator new[](size_t sz) { trace << "Widget::new[]: " << sz << " bytes" << endl; return ::new char[sz]; } void operator delete[](void* p) { trace << "Widget::delete[]" << endl; ::delete []p; } }; int main() { trace << "new Widget" << endl; Widget* w = new Widget; trace << "\ndelete Widget" << endl; delete w; trace << "\nnew Widget[25]" << endl; Widget* wa = new Widget[25]; trace << "\ndelete []Widget" << endl; delete []wa; }
- new und delete Deklaration werden um Klammern
[]
ergänzt - das Programm erzeugt folgende Ausgabe:
new Widget Widget::new: 40 bytes * delete Widget ~Widget::delete new Widget[25] Widget::new[]: 1004 bytes ************************* delete []Widget ~~~~~~~~~~~~~~~~~~~~~~~~~Widget::delete[]
- in den 4 zusätzlicen Bytes wird die zusätzliche Information über die Objektanzahl gespeichert
- dies ist aus der Unterschied zwischen
delete Widget= und ==delete []Widget
, a im ersten Fall Speicher für ein Widget Objekt freigegeben wird und hingegen im zweiten Fall die 4 zusätzlichen Bytes genutzt werden, um das Array von Objekten richtig freizugeben - falls der Speicheralloziierung in new Operator fehlschlägt, wird der entsprechende Konstruktor nicht aufgerufen:
#include <iostream> #include <new> // bad_alloc definition using namespace std; class NoMemory { public: NoMemory() { cout << "NoMemory::NoMemory()" << endl; } void* operator new(size_t sz) throw(bad_alloc){ cout << "NoMemory::operator new" << endl; throw bad_alloc(); // "Out of memory" } }; int main() { NoMemory* nm; try { nm = new NoMemory; } catch(bad_alloc) { cerr << "Out of memory exception" << endl; } }
Placement new and delete:
Gründe für placement new
- das Objekt an einer spezifischen Stelle zu positionieren
- verschiedene Speicherbereiche zu benützen, wenn new aufgerufen wird
- shared memory
- Allokieren von Arrays und die damit verbundene Verwaltungsinformation
- Zuweisungsoperator für Klassen mit Referenzen
struct A{ A( Foo& foo ):my_FooReference(foo){}; Foo& m_myFooReference; A& operator=( const A& copyFrom ){ if(this != ©From) { // explizite Destruierung this->~A(); // erzeuge in this Speicher new(this) A(copyFrom); } return *this; }
Syntax
void* operator new(size_t, void *point)
Neben dem impliziten ersten Argument kann die Adresse eines Speichbereichs oder eine Referenz einer Speicher allozierenden Funktion oder Objekts übergeben werden.
- Folgende beide Ausdrück sind äquivalent:
void* raw = allocate(sizeof(Foo)); Foo* p = new(raw) Foo(); Foo* p= new Fow();
Beispiel
Alloziere ein Objekt an einer vorgegebenen Speicherstelle.
#include <cstddef> // Size_t #include <iostream> using namespace std; class X { int i; public: X(int ii = 0) : i(ii) { cout << "this = " << this << endl; } ~X() { cout << "X::~X(): " << this << endl; } void* operator new(size_t, void* loc) { return loc; } }; int main() { int l[10]; cout << "l = " << l << endl; X* xp = new(l) X(47); // X at location l xp->X::~X(); // Explicit destructor call // ONLY use with placement! }
- delete xp kann hier nicht angewandt werden, da xp nicht auf dem heap angelegt wurde
- vgl. hierzu folgende äquivalente Ausdrücke:
delete xp; xp->~X(); operator delete(xp);
- placement new ist der einzige legitime Grund den Destruktor explizit aufzurufen
- falls der Konstruktor von X fehlschlägt, entsteht ein Speicherloch, denn
delete X
kann vom Laufzeitsystem nicht aufgerufen werden, da dies keine entsprechender Destruktor zum placement new ist - stelle einen entsprechenden placement delete Operator bereit, der die zum placement new korrespondierende Arbeit erledigt
Syntax
void operator delete(size_t, void* loc)
#include <iostream> int flag = 0; typedef unsigned int size_t; int test[10]; void operator delete(void* p, int ){ // int ist nur ein dummy Argument für die Korrespondenz zum placement new flag = 1; } void* operator new(size_t s, int i){ return &test[i]; } class A { public: A() {throw -37;} }; int main(){ try { A* p = new (0) A; } catch (int i) { } std::cout << "flag: " << flag << std::endl; }
- nun verhält sich placement new Standardkonform, den der Compiler erzeugt aus einem einfachen Aufruf der Form
Foo* p = new Foo()
semantisch folgenden Code:
Foo* p; // Exception bad_alloc oder einen Pointer auf den angeforderten Speicher void* raw = operator new(sizeof(Foo)); //fange jede Execption, die durch den Konstruktor ausgelöst wird try{ p = new(raw) Foo(); } catch (...) { // gib den Speicher frei und "rethrow" operator delete(raw); throw; }
Weiterlesen...