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

  1. nur in Ausdrücken mit eigenen Datentypen kann man Operator überladen
    right man kann den = << = Operator nicht so überladen, sodass = 1.414 << 2 = vom Compiler angenommen wird
  2. 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
  3. 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.
  1. die Präzedenz der Operatoren kann nicht verändert werden
  2. die Anzahl der Argumente eines Operators steht fest

Syntax

  • um den Operator @ zu überladen, wird eine Funktion operator@ 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
      right 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

  1. Argument: konstante Referenz, falls nur lesend darauf zugegriffen wird
  2. Memberfunktion: const, falls keine Veränderung an dem Objekt (Operanden) geschieht
  3. Returnwert:
    1. "by Value" right 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
    2. "by Reference" right 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()
  4. Logische Operatoren sollten ein bool Objekt zurückliefern
  5. Returnwertoptimierung:
    1. 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
    2. =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

OperatorEmpfohlener 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 Ausdruck 1 + a
    • obige main Funktion müsste umgeschrieben werden:
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 Klasse OldType erkläre ich, dass die implizite Konvertierung OldType zu NewType 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
  • 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
  • beim Löschen des Objekts fallen entsprechend wieder zwei Operationen an:
    • der Destruktor operator delete() wird aufgerufen
    • der Speicher wird wieder freigegeben
  • 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
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 an
  • bool 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 != &copyFrom)
  {
    // 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;  
}
  • MOVED TO... 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;  
 }

Mentoring

Stay Informed about my Mentoring

 

Rezensionen

Tutorial

Besucher

Heute 897

Gestern 3357

Woche 12460

Monat 39777

Insgesamt 3892491

Aktuell sind 34 Gäste und keine Mitglieder online

Kubik-Rubik Joomla! Extensions

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare