Advanced   Java   Services operatoroverloading



operatoroverloading

Sinnvoll eingesetztes Operatoroverloading kann die Lesbarkeit eines Programms deutlich verbessern. Für die meisten Opertoren gibt es zwei Möglichkeiten der Überladung, sie können entweder als friend-Funktionen überladen werden oder innerhalb von Klassen als Methoden (member-Funktionen).






Überladen eines binären Operators

Wir werden den Operator + überladen. Das Schema ist aber für alle binären Operatoren dasselbe.



Binärer Operator als friend-Funktion

In der Headerdatei

class Xxx
{
   //...
public:
   //...
   friend Xxx & operator+(const Xxx & a, const Xxx & b);
}

Implementierung in der cpp-Datei außerhalb der Klasse

Xxx & operator+(const Xxx & a, const Xxx & b)
{
   //...
}


Binärer Operator als member-Funktion

In der Headerdatei

class Xxx
{
   //...
public:
   //...
   Xxx & operator+(const Xxx & a);
}

Implementierung innerhalb der Klasse, der erste Operand ist *this der zweite Operand b

Xxx & Xxx::operator+(const Xxx & b)
{
   //...
}


Überladen eines unären Operators

Wir nennen den zu überladenen Operator @. Das Schema ist für alle unären Operatoren dasselbe.



Unärer Operator als friend-Funktion

In der Headerdatei

class Xxx
{
   //...
public:
   //...
   friend Xxx & operator@(const Xxx & a);
}

Implementierung in der cpp-Datei außerhalb der Klasse

Xxx & operator@(const Xxx & a)
{
   //...
}


Unärer Operator als member-Funktion

In der Headerdatei

class Xxx
{
   //...
public:
   //...
   Xxx & operator@();
}

Implementierung innerhalb der Klasse, der Operand ist *this

Xxx&Xxx::operator@()
{
   //...
}


Überladen von "++" und "--"

Für diese Operatoren gilt zwar das Schema für unäre Operatoren, jedoch müssen die Fälle "++x" und "x++" bzw. "--x" und "x--" unterschieden werden.



Prefix-Überladung

Das folgende kleine Beispiel überlädt den Prefixoperator einmal als member und einmal als friend.

// member
class Xxx
{
public:
   int a;
   Xxx & operator++();
};

Xxx & Xxx::operator++()
{
   ++a;
   return *this;
}

// friend
class Yyy
{
public:
   int b;
   friend Yyy & operator++(Yyy &);
};

Yyy& operator++(Yyy & y)
{
   ++y.b;
   return y;
}

int main()
{
   Xxx x;
   x.a = 17;
   cout << ++x.a << endl;  // 18

   Yyy y;
   y.b = 17;
   cout << ++y.b << endl;  // 18
   return 0;
}

Postfix-Überladung

Das folgende kleine Beispiel überlädt den Postfixoperator einmal als member und einmal als friend. Zur Unterscheidung des vorigen Falls wird ein zusätzlicher Parameter vom Typ int übergeben der nie verwendet wird.

// member
class Xxx
{
public:
   int a;
   Xxx & operator++(int i);
};

Xxx & Xxx::operator++(int i)
{
   a++;  // hier egal;
   return *this;
}

// friend
class Yyy
{
public:
   int b;
   friend Yyy & operator++(Yyy &, int i);
};

Yyy & operator++(Yyy & y, int i)
{
   ++y.b;
   return y;
}

int main()
{
   Yyy y;
   y.b = 17;
   cout << y.b++ << endl;  // 17
   cout << y.b << endl;  // 18
   return 0;
}

Für die Pre- und Postfixüberladungen wird die Variante mit Memberfunktionen bevorzugt.


Überladen von  <<  (etwa für cout)

Der Operator << kann nur über eine friend-Funktion überladen werden. Sie wird der Klasse wie folgt bekanntgemacht.

class Xxx
{
   //..
public:
   //..
   friend std::ostream& operator<< (std::ostream& out, const Xxx& xxx);
};

Das Schema der Implemetierung

std::ostream& operator<< (std::ostream& out, const Xxx& xxx)
{
   out << ...

   return out;
}

Überladen des Indexoperators [ ]

Der Indexoperator kann nur über Memberfunktionen überladen werden. Es gibt eine Memberfunktion für Lesen und eine für Schreiben.

class Xxx
{
   //...
   // [] überladen
   Xxx& operator[](int i); // schreibender Zugriff
   const Xxx& operator[](const int i) const;  // lesender Zugriff
};

Implementierung

// schreiben
Person& PersonList::operator[](int i)
{
   //...
}
// lesen
const Person& PersonList::operator[](const int i) const
{
   //...
}

Es ist allerdings compilerabhängig, ob die lese-Variante verwendet wird . VS 2010 und MInGW verwenden immer nur die schreibende Variante.


Überladen des function-call Operators

Mit dem function-call Operator ist es möglich ein Object wie eine Funktion zu verwenden. Dies wird gerne verwendet im Zusammenhang mit Templates. Beispiele hierzu findet man bei Uniquepointer und bei Sharedpointer. Der function-call Operator wird leicht verwechselt mit einem Konstruktoraufruf, hier muß man genau zwischen der Initialisierung und der Ebene der Statements unterscheiden. Ein einfaches Beispiel wird den Unterschied deutlich zeigen.

Unsere Beispielklasse ( die Operatorfunktion operator() wird auch gerne Functor genannt)

class Xxx
{
private:
   int x;
public:
   Xxx(int x = 0) : x(x)
   {
      cout << "Xxx(int x = 0) " << endl;
   }
   // function-call operator mit einem Argument
   void operator()(int i)
   {
      cout << "operator()(int)" << endl;
   }
   // function-call operator mit zwei Argumenten in der Parameterliste
   int operator()(int i, int j)
   {
      cout << "operator()(int, int)" << endl;
      return i+j;
   }
};

Das Programm

/*
 * Verwendet Xxx
 */
void beispiel1()
{
   cout << "oberloading function call" << endl << endl; //
   // Konstruktoraufrufe
   Xxx foo;
   Xxx bar(17);
   // function call Aufrufe
   foo(17);
   bar(71);
   cout << foo(17, 42) << endl << endl;
}

Die Ausgabe

Das Beispiel verwendet zwei Überladungen des function-call Operators. Die Parameterlisten können beliebig sein, solange sie unterscheidbar sind.

Der function-call Operator kann auch als Indexoperator fungieren, wenn der Returntyp eine Referenz ist. In diesem Falle macht er den Indexoperator überflüssig. Das nächste Beispiel zeigt dies.

class IntArray
{
private:
      int *ptr;
public:
   IntArray(size_t size) : ptr(new int[size])
   {
      cout << "IntArray(size_t size)" << endl;
   }
   ~IntArray()
   {
      delete[] ptr;
   }
   // schreiben
   int& operator[](int i)
   {
      cout << "operator[](int) write" << endl;
      return ptr[i];
   }
   // function call
   int& operator()(int index)
   {
      cout << "operator()(int)" << endl;
      return ptr[index];
   }
};

Das Programm

/*
 * verwendet IntArr
 */
void beispiel2()
{
   cout << "oberloading function call" << endl << endl; //

   IntArray intarr(10);
   // write  operator[]
   intarr[5] = 17;
   // read  operator[]
   cout << intarr[5] << endl;

   // write  operator()
   intarr(6) = 42;
   // read  operator()
   cout << intarr(6) << endl;
}

Die Ausgabe


Überladen des cast-Operators

Der cast-Operator kann nur als nichtstatische member-Funktion überladen werden. Die Überladung hat die folgende Form

operator <zieldatentyp>()

Man beachte, daß es keinen Returntyp gibt. Trotzdem muß der Funktionsrumpf mit einer Return-Anweisung enden.

In der folgenden Header-Datei deklarieren wir eine Klasse Test. Die Klasse hüllt ein int ein und enthält einen cast-Operator sowie eine friend-Funktion für die Ausgabe.

#pragma once
#include <iostream>

using namespace std;

class Test
{
   int a;

public:

   Test(void) : a(0)
   {
      cout << "default constructor" << endl;
   }

   Test(int i) : a(i)
   {
      cout << "1-arg ctor" << endl;
   }

  // overloaded int cast , no return type, must be a non static member function, converts Test to int
  operator int() const
   {
      cout << "operator int()" << endl;
      return a;
   }

   friend ostream & operator<<(ostream &out, const Test& p)
   {
      cout << "operator<<()" << endl;
      return out << p.a ;
   }

   virtual ~Test(void)
   {}
};

Ein Beispielprogramm

int main()
{
   printf("overloading the cast-operator\n\n");

   Test t(17);
   int a = t;
   cout << t << endl;
}

Die Ausgabe

Man beachte, daß in diesem Fall die Funktion opertor<< nicht notwendigerweise überladen werden muß, da für die Ausgabe auch der cast-Operator verwendet werden kann. Kommentiert man die friend-Funktion aus, so ergibt sich die folgende Ausgabe.


Verhindern des impliziten cast mit Hilfe des Schlüsselworts explicit

Ein automatischer cast kann unerwartete Nebeneffekte provozieren. Mit Hilfe des Schlüsselworts explicit kann der Entwickler implizites casten unterbinden. In diesem Fall muß der cast dann explizit gesetzt werden. Durch ergänzen von explicit am Anfang der Signatur kann man den impliziten cast verhindern. Hat man die Ausgabe überladen braucht man an dieser Stelle aber keinen expliziten cast. Das folgende Beispiel zeigt dies.

Wir ändern im Header nur den cast-Operator und ergänzen hier explicit.

...

  // overloaded int cast, using explicit to prevent implicit casts
  explicit operator int() const
   {
      cout << "operator int()" << endl;
      return a;
   }

...

In main muß nun explizit gecastet werden.

int main()
{
   printf("overloading the cast-operator\n\n");

   Test t(17);
   int a = (int)t;
   cout << t << endl;
}











Überladen von new und delete für Klassen

Im folgenden kleinen Beispiel werden new und delete für eine Klasse überladen. In diesem Falle werden die Operatorfunktionen vom Compiler implizit static angelegt.

Die Klasse

class Test
{
public:
   int i;

   Test(int i=0) : i(i)
   {}
   // allocate
   void * operator new(size_t size)  // implizit static
   {
      cout << "new size = " << size << endl;
      return malloc(size);
   }
   // delete
   void operator delete(void * mem)  // implizit static
   {
      cout << "delete " << endl;
      free(mem);
   }
};

Ein Testprogramm

int main()
{
   cout << "new and delete" << endl; //
   Test * pt = new Test(42);
   cout << pt->i << endl;
   delete pt;
   return 0;
}

Die Ausgabe


Überladen von new und delete global

Verwendet man new für Standarddatentypen, etwa mit int * arr = new int;, so wird ein globales new bzw. ein globales delete verwendet. Auch diese Operatorfunktionen kann man überschreiben. In einfacher Form sieht das so aus:

// global
void* operator new(size_t sz) 
{
    cerr << "allocating " << sz << " bytes\n";
    void* mem = malloc(sz);
    if (mem)
        return mem;
    else
        throw std::bad_alloc();
}

void operator delete(void* ptr) 
{
    cerr << "deallocating at " << ptr << endl;
    free(ptr);
}

Diese Funktionen sind dem Compiler bekannt, eine Deklaration ist nicht notwendig. Zum Testen verwendet man new und delete wie bisher.

int main()
{
   cout << "new and delete global" << endl; //

   int * arr = new int;
   delete arr;

   return 0;
}

Die Ausgabe




Überladen des Zuweisungsoperators (rule of three)

Der Zuweisungsoperator hat die folgende feststehende Signatur.

Xxx& operator=(const Xxx& a)

Hat eine Klasse mehrere Datenmember, so muß meistens der Zuweisungsoperator überladen derden, da der Defaultzuweisungsoperator lediglich eine flache Kopie erzeugt. In diesem Zusammenhang reicht dann auch der Defaultkopierkonstruktor nicht aus und muß ebenfalls überladen werden. Ebenso muß in dieser Situation meist Speicher freigegeben werden was bedeutet, daß auch der Destruktor selbst geschrieben werden muß. Dies hat zu der in der Überschrift erwähnten "rule of three" geführt.

rule of three

Falls man eine der drei Funktionen Defaultkopierkonstruktor, Defaultzuweisungsoperator, Destruktor überschreibt,
so ist es in der Regel notwendig, auch die beiden anderen Funktionen selbst zu implementieren.


Der Kopierkonstruktor

Auch der Kopierkonstruktor hat eine feststehende Signatur.

Xxx(const Xxx& a)

Im nächsten Beispiel zeigt die Folgen einer flachen Kopie, die der Defaultzuweisungsoperator macht.

class X
{
public:
   char* name;

   X(char* name)
   {
      this->name = new char[strlen(name)+1];
      strcpy(this->name, name);
   }
};

Eine kleine Testfunktion zeigt den Fehler

void testX()
{
   X x("hallo");
   cout << x.name << endl;  // hallo

   X y = x;
   cout << y.name << endl;  // hallo  OK

   x.name[1] = 'e';
   cout << x.name << endl;  // hello

   cout << y.name << endl;  // hello NOK !!
}

Der Fehler wird behoben in dem wir die rule of three beherzigen.

class XX
{
public:
   char* name;
   // constructor
   XX(char* name)
   {
      this->name = new char[strlen(name)+1];
      strcpy(this->name, name);
   }
   // copy-constructor
   XX(const XX& a)
   {
      this->name = new char[strlen(a.name)+1];
      strcpy(this->name, a.name);
   }
   // Zuweisungsoperator
   XX& operator=(const XX& a)
   {
      this->name = new char[strlen(a.name)+1];
      strcpy(this->name, a.name);
   }
   // Destruktor
   ~XX()
   {
      delete name;
   }
   void println()
   {
      cout << this->name << endl;
   }

};

Die Testfunktion zeigt den Erfolg

void testXX()
{
   XX xx("hallo");
   xx.println();   // hallo

   XX yy = xx;
   yy.println();   // hallo  OK

   xx.name[1] = 'e';
   xx.println();   // hello OK

   yy.println();   // hallo OK !!
}

Tabelle der nicht überladbaren Operatoren

Die die meisten Operatoren überladen werden können, hier eine Tabelle der nicht überladbaren Operatoren.




Präzedenz Operator  Überladbar Bezeichnung  Assoziativität  Operandentyp 
 1 Bereichsauflösung  [ C++ ]
 ::  nein  Bereichsauflösung  links  Klasse, Struktur, Union, Member
 .  nein  Punktoperator  links  Klasse, Struktur, Union, Member
 const_cast<>( )  nein  Constcast  [ C++ ]  links  Variable beliebigen Datentyps
 dynamic_cast<>( )  nein  Dynamiccast  [ C++ ]  links  Variable beliebigen Datentyps
 reinterpret_cast<>( )  nein  Reinterpretcast  [ C++ ]  links  Variable beliebigen Datentyps
 static_cast<>( )  nein  Staticcast  [ C++ ]  links  Variable beliebigen Datentyps
 typeid( )  nein  typeid  [ C++ ]  links  Variable beliebigen Datentyps
 3 Unäre Operatoren
 sizeof  nein  Größenoperator  rechts  alle
 15 bedingter Ausdruck
 ? :  nein  bedingter Ausdruck  rechts  boolesch ? alle : alle
 17 Exception werfen  [ C++ ]
 throw   nein  Exception werfen  rechts  Exceptionobjekte