C++/Tutorial: 13. Weitere Eigenschaften von Klassen II

Aus Scientia
Version vom 17. Januar 2010, 13:18 Uhr von Ankou (Diskussion | Beiträge)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

Und es verbleiben immernoch einige Eigenschaften von Klassen unerklärt. In diesem Abschnitt möchte ich auf die Überladung von Operatoren und auf eine eigene Defintion von Typecasts eingehen.

Operatorenüberladung

Wie bereits angesprochen ist es möglich in C++ den bereits existierenden Operatoren eine neue Bedeutung zuzuweisen. Operatoren als solche sind im Prinzip nichts anderes als normale Methoden, die mit einer speziellen Syntax aufgerufen werden. Nehmen wir einmal eine Klasse, die einen zweidimensionalen Vektor im mathematischen Sinne darstellen soll. Also eine Klasse mit zwei Membervariablen: x und y.

class mvector2
  {
  public:
    mvector2(int x, int y);
    int x() const;
    int y() const;
  private:
    int m_x, m_y;
  };
mvector2::mvector2(int x, int y) : m_x(x), m_y(y) 
  {}
int mvector2::x() const
  {
  return m_x;
  }
int mvector2::y() const
  {
  return m_y;
  }

wir können damit schon einen Punkt darstellen, nun macht es aber Sinn, diese Vektoren addieren zu können, dabei müsste nichts weiter geschehen als ein neuer Vektor erstellt mit x und y als Summe der beiden Summandenvektoren. Erstellen wir nun zum Beispiel eine add-Methode mit uns bekannten Mitteln.

class mvector2
  {
  public:
    mvector2(int x, int y);
    int x() const;
    int y() const;
    mvector2 add(mvector2 const& that) const;
  private:
    int m_x, m_y;
  };
 
mvector2 mvector2::add(mvector2 const& that) const
  {
  return mvector2(m_x + that.x(), m_y + that.y());
  }

Die Verwendung sollte klar sein:

int main()
  {
  mvector2 some_point(3,2);
  mvector2 some_offset(1,1);
  mvector2 some_new_point(some_point.add(some_offset)); //addiere beide Vektoren, das Resultat ist some_point um some_offset verschoben, sprich (4,3)
  }

Das ganze funktioniert gut und wir werden an der Funktionalität in diesem Beispiel auch nichts mehr ändern, die Syntax ist jedoch wenig schön. Eine Methode add ist wenig intuitiv und nicht unbedingt das leserlichste, besser wäre es könnten wir some_point + some_offset schreiben. Interessanterweise müssen wir dafür gar nicht mehr viel ändern, lediglich der Name der Funktion muss geändert werden in operator+ und schon handelt es sich um einen überladenen Operator:

class mvector2
  {
  public:
    mvector2(int x, int y);
    int x() const;
    int y() const;
    mvector2 operator+(mvector2 const& that) const;
  private:
    int m_x, m_y;
  };
 
mvector2 mvector2::operator+(mvector2 const& that) const
  {
  return mvector2(m_x + that.x(), m_y + that.y());
  }
 
int main()
  {
  mvector2 some_point(3,2);
  mvector2 some_offset(1,1);
  mvector2 some_new_point(some_point + some_offset); 
  }

Freie Operatoren

Wenn wir unser Beispiel betrachten macht es allerdings wenig Sinn, dass add oder operator+ eine Memberfunktion ist. Besser wäre es doch eine freistehende Funktion zu haben, hätten wir immernoch eine normale Funktion add wäre dies einfach, wir würden einfach eine Funktion

mvector2 add(mvector2 const& a, mvector2 const& b)
  {
  return mvector2(a.x() + b.x(), a.y() + b.y());
  }

Aber geht das auch so einfach mit Operatorenüberladung? nun.. ja:

class mvector2
  {
  public:
    mvector2(int x, int y);
    int x() const;
    int y() const;
  private:
    int m_x, m_y;
  };
 
mvector2 operator+(mvector2 const& a, mvector2 const& b);
 
 
mvector2::mvector2(int x, int y) : m_x(x), m_y(y) 
  {}
int mvector2::x() const
  {
  return m_x;
  }
int mvector2::y() const
  {
  return m_y;
  }
 
mvector2 operator+(mvector2 const& a, mvector2 const& b)
  {
  return mvector2(a.x() + b.x(), a.y() + b.y());
  }
 
int main()
  {
  mvector2 some_point(3,2);
  mvector2 some_offset(1,1);
  mvector2 some_new_point(some_point + some_offset); 
  }

Operatoren als Member nehmen immer das Objekt selbst als ersten Operanden, wir können es aber auch außerhalb der Klasse definieren, wie in dem Beispiel und haben dann 2 Parameter für die Operanden.

Mögliche Operatoren

Nun stellt sich die Frage, welche Operatoren wir nun alles überladen können. Ohne spezielle Aufmerksamkeit können wir natürlich jeden mathematischen, logischen und auch jeden Vergleichsoperator auf die selbe Weise überladen. Auch unäre Operatoren wie !, ++, -- oder das unäre + und -(Vorzeichen) lassen sich überladen, nehmen dann eben nur einen Parameter als Operanden.

Indexoperator

Einige Operatoren, sind noch einmal von besonderer Bedeutung, oder Interesse. So zum Beispiel der Indexoperator, dieser ist ebenso zu überladen, wie alle anderen Operatoren auch. (some_object operator[](size_t index) wäre ein passender Funktionskopf). Interessant ist er deswegen, weil er erklärt, wie der Zugriff auf Container, wie std::vector mithilfe von [], wie bei arrays möglich ist. Damit ist wieder ein Stück der Klassen der Standardbibliothek demystifiziert.

()-Operator

Eine große Besonderheit stellt der () Operator dar, der sich ebenfalls überladen lässt. Ihn werde ich in einem späteren Kapitel noch einmal gesondert ansprechen.

->-Operator

Es ist auch möglich sowohl den Dereferenzierungsoperator *, als auch den Dereferenzierungsoperator -> zu überladen (zusätzlich gibt es noch einen Operator ->*, der normalerweise aber nicht überladen wird, da es reicht * und -> zu überladen). Das ist nötig für die Smartpointer. Aber wieso kriegt der Dereferenzierungsoperator -> eine eigenes Unterkapitel, der Operator * aber nicht? -> funktioniert anders als die anderen Operatoren. Dieser Operator soll den Zugriff auf Funktionen des Objektes ermöglichen, wie kann man so etwas überladen? Die Funktion operator-> erledigt lediglich die Dereferenzierung und ist damit unär. Auf den Rückgabewert (einen Pointer, oder ein anders Objekt für den dieser Operator überladen ist) wird dann die entsprechende Funktion aufgerufen. Ein kleines Beispiel(praktisch nutzloses): eine Wrapperklasse, die den Aufruf von Methoden mit -> mitloggt und an eine Membervariable weiterleitet:

class bar
  {
  public:
    bar(std::vector<int> const& some_object);
    std::vector<int>* operator->();
  private:
    std::vector<int> m_some_object;
  };
 
using namespace std;
 
bar::bar(vector<int> const& some_object) : m_some_object(some_object)
  {}
 
vector<int>* bar::operator->()
  {
  cout << "call some method..." << endl;
  return &m_some_object;
  }
 
int main()
  {
  vector<int> example_vector(1, 3);
  bar b(example_vector);
  cout << b->at(0) << endl;
  }

Überladen des << Operators für die Ausgabe in Streams

Kommen wir nun zurück zu unserem Beispiel mit dem Vektor, es wäre praktisch, wenn wir ihn z.B. Testzwecken in der Konsole ausgeben könnten. Wir könnten dazu eine Methode to_string() einführen, die aus dem Vektor einen String macht, aber praktischer und intuitiver wäre es doch, wenn wir direkt cout << some_vector; schreiben könnten. Dazu müssen wir keine neue Syntax lernen, wir wissen, dass es sich hier nur um einen überladenen << Operator handelt. Also schauen wir was für Parameter denn sinnvoll wären. Das erste Argument wird cout sein, cout ist ein Objekt vom Typ ostream, wir benötigen eine Referenz darauf das zweite Argument wäre unser mvector, Rückgabetyp muss wieder ostream& sein, denn es ist ja möglich die << Aufrufe zu verketten, also sähe unser Funktionskopf in etwa so aus:

std::ostream& operator<<(std::ostream& out, mvector2 const& vec);

das ist auch schon alles, was wir benötigen um eine entsprechende Funktion zu entwerfen. Wir müssen uns noch Gedanken machen, wie die Ausgabe aussehen soll, ich habe mich an dieser Stelle für [x;y] entschieden, somit:

#include <iostream>
 
class mvector2
  {
  public:
    mvector2(int x, int y);
    int x() const;
    int y() const;
  private:
    int m_x, m_y;
  };
 
mvector2 operator+(mvector2 const& a, mvector2 const& b);
 
std::ostream& operator<<(std::ostream& out, mvector2 const& vec);
 
using namespace std;
 
mvector2::mvector2(int x, int y) : m_x(x), m_y(y) 
  {}
int mvector2::x() const
  {
  return m_x;
  }
int mvector2::y() const
  {
  return m_y;
  }
 
mvector2 operator+(mvector2 const& a, mvector2 const& b)
  {
  return mvector2(a.x() + b.x(), a.y() + b.y());
  }
 
ostream& operator<<(ostream& out, mvector2 const& vec)
  {
  return out << "[" << vec.x() << ";" << vec.y() << "]";
  }
 
int main()
  {
  mvector2 some_point(3,2);
  mvector2 some_offset(1,1);
  mvector2 some_new_point(some_point + some_offset); 
  cout << some_new_point << endl; //gebe das Resultat aus
  }

Benutzerdefinierte Casts

C++ erlaubt uns außerdem selbst Typcasts für static_cast zu definieren. Nehmen wir als Beispiel mal eine einfache Wrapperklasse, die ein int wrappt:

class int_wrapper 
  {
  public:
    int_wrapper(int i);
  private:
    int m_i;
  };
 
int_wrapper::int_wrapper(int i) : m_i(i)
  {}

wenn wir es uns genauer betrachten haben wir damit schon einen Cast definiert. Dadurch, dass wir einen Konstruktor definiert haben, der einen int nimmt ist C++ in der Lage jeden int implizit in einen int_wrapper zu casten. Wann immer eine Funktion ein Objekt vom Typ int_wrapper verlangt ist es möglich ein int zu übergeben:

void foo(int_wrapper const& iw)
  {
  //...
  }
 
int main() 
  {
  foo(4); //implizit
  foo(static_cast<int_wrapper>(6)); //explizit
  }

explicit

Wie man sehen kann sind auch explizite Casts möglich(eigentlich selbstverständlich). Es ist aber auch möglich implizite Casts zu verbieten und nur explizite Casts(oder eben direkte Aufrufe des Konstruktors) zuzulassen: das Schlüsselwort explicit. Es wird dem Konstruktor vorangestellt und schon:

class int_wrapper 
  {
  public:
    explicit int_wrapper(int i);
  private:
    int m_i;
  };
 
int_wrapper::int_wrapper(int i) : m_i(i)
  {}
 
void foo(int_wrapper const& iw)
  {
  //...
  }
 
int main() 
  {
  //foo(4); //implizit - nicht mehr möglich
  foo(static_cast<int_wrapper>(6)); //explizit
  }

Casts in andere Typen

Bisher haben wir nur die Möglichkeit beliebige Typen in unseren zu casten, aber was wenn wir es andersrum wollen? Es ist uns auf diese Art schwer möglich einen int_wrapper in einen int zu casten, aber auch das ist möglich. Die Funktion, die wir dazu schreiben müssen, sieht aus wie eine Operatorüberladung und heißt operator int (int durch den Typ ersetzen, in den wir casten wollen, logischerweise). Rückgabetyp muss nicht angegeben werden. Für uns sieht die Deklaration also so aus:

operator int () const;

die Implementierung der Funktion gibt dann den entsprechen int zurück, alles in allem sieht das Beispiel folgendermaßen aus:

#include <iostream>
 
using namespace std;
 
class int_wrapper 
  {
  public:
    int_wrapper(int i);
    operator int () const; 
  private:
    int m_i;
  };
 
int_wrapper::int_wrapper(int i) : m_i(i)
  {}
 
int_wrapper::operator int() const 
  {
  return m_i;
  }
 
int main() 
  {
  int_wrapper iw(3);
  cout << iw << endl; //Die Ausgabe ist für int_wrapper nicht definiert, aber für int. C++ castet iw hier also in ein int.
  }

Etwas wie explicit gibt es für Casts in dieser Richtung noch nicht, soll im nächsten Standard aber eingeführt werden.