Sie sind hier: Buchauszüge Typwandlungen  
 BUCHAUSZÜGE
Vorwort
Inhaltsverzeichnis
Einführung
Typwandlungen

BENUTZERDEFINIERTE WANDLUNGEN
 
&&

Die Funktionalität der C++-Wandlungsoperatoren ist festgelegt und kann vom Programmierer nicht verändert werden. Sie werden meist im Zusammenhang mit fundamentalen Typen sowie mit Zeiger- bzw. Referenztypen auf Klassen verwendet.

Ist der Quell- bzw. Zieltyp eine Klasse, kann der Programmierer die Vorgänge bei der Typwandlung selber definieren. Dazu stehen prin­zipiell zwei Möglichkeiten bereit: Konstruktoren und Wandlungsope­ratoren. In beiden Fällen läuft eine erforderlich Wandlung prinzi­piell[1] implizit, d.h. automatisch ohne Zutun des Programmierers ab. Hinzu kommt natürlich die „klassische“ Lösung durch Formulierung der Wandlung in einer normalen Funktion, die dann allerdings expli­zit aufgerufen werden muss.

Wir betrachten im folgenden die generelle Aufgabenstellung, ein Ob­jekt eines Typs A implizit in ein Objekt eines anderen Typs B zu wandeln, damit Anweisungsfolgen wie in

 

void f( B& b );

A a;

f( a );                // nur OK, wenn implizite Wandlung A nach B möglich ist!

 

möglich sind. Funktion f benötigt ein B-Objekt, auf das eine Referenz übergeben werden kann, der Aufruf erfolgt jedoch mit einem Objekt vom Typ A. Der Aufruf ist zulässig, wenn eine implizite Wandlung von A nach B definiert ist.

Für die folgenden Abschnitte betrachten wir folgende beispielhafte Implementierungen der Klassen A und B:

 

struct A

{

  int    i;

  double d;

};

 

struct B

{

  string s;

};

 

Beide Klassen sind bewusst einfach gehalten (z.B. zunächst ohne Konstruktoren) um das Wesentliche zu demonstrieren.

 

&   Die klassische Variante

Man kann für jede erforderliche Wandlung immer eine spezielle Funktion schreiben. Um A-Objekte in B-Objekte zu wandeln, dekla­riert man eine Wandlungsfunktion fromAToB als

 

B fromAToB( const A& );

 

In unserem Fall könnte man die Funktion wie folgt implementieren:

 

//-- l 01

//  klassische Wandlungsfunktion

 

B fromAToB( const A& a_in )

{

  B result;

 

  char buf[ 32 ];

  sprintf( buf, "i: %i, d: %f", a_in.i, a_in.d );

  result.s = buf;

 

  return result;

}

 

Die Wandlung kann dann allerdings nicht implizit erfolgen, sondern muss explizit notiert werden. Das einführende Beispiel nimmt nun die Form

 

void f( B& b );

 

void g()

{

  A a;

  f( fromAToB( a ));       // OK, da expliziter Aufruf einer Wandlungsfunktion

}

 

an.

&   Exkurs: Die printf-Funktionsfamilie

Die Funktion sprintf führt eine Ausgabe ihrer Argumente in einen Speicherbereich durch, der als erstes Argument zu übergeben ist. Der zweite String ist ein Formatstring, der eingestreute Sonderzeichen (hier %i und %f) enthalten kann. Für diese Platzhalter werden nach­folgende Parameter eingesetzt. Die Sonderzeichen bestimmen, wel­cher Typ erwartet wird. %i steht für int, %f für double. Im Endeffekt wird hier also eine Umwandlung der numerischen Werte in eine Stringrepräsentation durchgeführt und das Ergebnis mit zusätzlichem Text dekoriert.

Dies ist wohl auch eine der Hauptaufgaben von Funktionen der printf-Familie: sie können gut zur Umwandlung von numerischen Größen in eine Stringrepräsentation verwendet werden. Beachten Sie bitte, dass der Aufrufer einen Puffer bereitstellen muss, der groß ge­nug für die Ausgabe ist. sprintf führt keine Überprüfungen durch.

 

Übung 24-5:

Was passiert, wenn der Puffer zu klein ist?

 

Eine weitere Problematik ist die Angabe der Platzhalter im Formatie­rungsstring. Folgende Anweisung ist syntaktisch zulässig, produziert jedoch undefiniertes Verhalten:

 

sprintf( buf, "i: %i, d: %f", a_in.d, a_in.i );

 

Der Fehler ist auf den ersten Blick schwer zu erkennen: hier wurde versucht, die double-Größe a_in.d mit dem Formatierer %i aus­zugeben (und analog die int-Größe a_in.i mit dem Formatierer %f) – was natürlich nicht funktioniert[2].

&   Wandlung über Konstruktoren

Die Wandlung kann implizit erfolgen, wenn B mit einem geeigneten Konstruktor  ausgestattet wird. Schreibt man

 

struct B

{

  B( const A& );

  string s;

};

 

kann man wie erwartet B-Objekte mit A-Objekten initialisieren:

 

A a;

B b( a );           // Initialisierung von b mit a

 

Das wesentlich Neue daran ist nun, dass der Konstruktor auch in Si­tuationen wie

 

void f( const B& );

 

void g()

{

  A a;

  f( a );           // OK

}

 

implizit verwendet wird.

Konkret erzeugt der Compiler hier ein temporäres B-Objekt, initiali­siert es mit a und übergibt eine Referenz auf das temporäre Objekt an f. Es ist Aufgabe des Compilers, das temporäre Objekt auch wieder zu zerstören.

Beachten Sie bitte, dass der Parameter der Funktion f als const B& ausgeführt ist. Die Deklaration als Referenz auf nicht-konstant funkti­oniert nicht, da temporäre Objekte nur an konstante Referenzen ge­bunden werden können.

 

void f( B&  );

 

void g()

{

  A a;

  f( a );           // Fehler

}

 

Der Grund ist, dass f das B-Objekt nun ändern könnte, diese Ände­rungen sich aber nirgendwo niederschlagen: das temporäre Objekt verschwindet ja nach Ausführung von f wieder[3]. Die Änderung ei­nes temporären Objekts wird daher als Fehler gewertet und abgelehnt.

In Analogie zu unsere Wandlungsfunktion fromAtoB implementieren wir den Wandlungskonstruktor wie folgt:

 

B::B( const A& a_in )

{

  char buf[ 32 ];

  sprintf( buf, "i: %i, d: %f", a_in.i, a_in.d );

  s = buf;

}

 

Die Funktion g kann nun einfacher folgendermaßen geschrieben werden:

 

void f( B& b );

 

void g()

{

  A a;

  f( a );    // OK: implizite Wandlung über Wandlungskonstruktor

}

 

 

 

Übung 24-6:

Funktioniert die implizite Wandlung auch dann, wenn der Kon­struktor mehrere Argumente hat, von denen alle bis auf eines mit Vor­gabewerten versehen sind? Beispiel:

 

struct B

{

  B( const A&, int i = 0 );

  string s;

};

 

 

Übung 24-7:

Wird die Wandlung auch in anderen Situationen wie z.B. der Zuwei­sung implizit durchgeführt? Ist eine Zuweisung wie in

 

A a;

B b;

b = a; // ist dies zulässig?

zulässig, auch wenn kein Zuweisungsoperator für B definiert ist?

 

Übung 24-8:

Welche Auswirkungen hat es, wenn das Argument im B-Konstruktor nicht konstant ist?

 

struct B

{

  B( A& );   // nicht konstantes Argument

  string s;

};

 

 

&   Konstruktoren mit mehreren Parametern

Die Wandlung kann nur implizit ablaufen, wenn der Konstruktor des Zieltyps mit genau einem Argument des Quelltyps aufgerufen werden kann. Sind weitere Argumente erforderlich, ist ein impliziter Aufruf nicht mehr möglich.

Der Programmierer muss den Konstruktor explizit aufrufen und dabei die zusätzlichen Parameter spezifizieren:

 

struct B

{

  B( const A&, int i );

  string s;

};

 

void f( const B& );

 

void g()

{

  A a;

  f( B(a,1) );             // OK – explizite Angabe des Konstruktors

}

 

Die Vorgänge sind analog zum impliziten Fall mit einem Parameter wie im letzten Abschnitt. Insbesondere wird auch hier ein temporäres B-Objekt erzeugt. Der Unterschied ist lediglich, dass nun der Aufruf des Konstruktors explizit erfolgen muss. In diesem Sinne handelt es sich auch hier um eine Art „Typwandlung“: Es wird das Tupel (a,1) in ein Objekt vom Typ B gewandelt.

&   Das Schlüsselwort explicit

Die implizite Wandlung über einen Konstruktor mit einem Argument ist nicht immer erwünscht, auch wenn man auf den Konstruktor sel­ber nicht verzichten möchte. Die Anweisungen

 

void f( const B& );

 

void g()

{

  A a;

  f( a );    // unerwünscht

}

 

können auch schlicht ein Fehler sein – der Programmierer meinte eher

 

void f( const B& );

 

void g()

{

  B b;

  f( b );

}

 

In der Praxis sind die Fälle meist nicht so offensichtlich wie in diesem Beispiel. Oft übersetzen (komplexe) Funktionsaufrufe, Zuweisungen etc, obwohl sie eigentlich nicht sollten. Der Grund liegt oft in einem Konstruktor, der mit einem Parameter aufgerufen werden kann und damit zu einer (unerwünschten) automatischen Typwandlung ver­wendet wird.

Ein Standardfall für eine solche Situation kann an unserer Klasse Fi­xedArray studiert werden, die ja die Größe des Feldes als Parameter im Konstruktor erhält:

 

class FixedArray

{

public:

  FixedArray( int dim_in );

 

  /* ... weitere Mitglieder FixedArray */

};

 

Eine Funktion, die ein solches Feld übernehmen soll, wird z.B. als

 

void g( const FixedArray& );

 

deklariert. Nun sind neben erwünschten Aufrufen der Art

 

FixedArray fa( 3 );

g( fa );            // Übergabe einer Referenz auf fa an g

 

auch unerwünschte Aufrufe wie

 

g( 5 );                    // OK, aber unerwünscht

 

möglich. In diesem Fall findet eine „Typwandlung“ von int zu Fi­xed­Array statt, da der Zieltyp FixedArray einen geeigneten Wand­lungskonstruktor deklariert.

Was man also benötigt ist eine Möglichkeit, den Konstruktor zu be­halten, ihn aber von automatischen Typwandlungen auszuschließen. Genau dies leistet die Deklaration als explicit:

 

class FixedArray

{

public:

  explicit FixedArray( int dim_in );   // keine impliziten Typwandlungen

 

  /* ... weitere Mitglieder FixedArray */

};

 

Nun funktioniert zwar

 

FixedArray fa( 3 );// OK

 

nicht aber

 

g( 5 );             // Fehler! keine implizite Wandlung mehr möglich

 

Beachten Sie bitte, dass die explizite Notation weiterhin möglich bleibt:

 

g( FixedArray( 5 ) );      // OK – explizite „Wandlung“

 

 

Übung 24-9:

Kann die Deklaration von Konstruktoren als explicit auch für die Klassen FractInt und Complex sinnvoll sein?

 

Weiterhin interessant ist, dass bei explicit deklarierten Konstrukto­ren einen Unterschied zwischen den Aufrufen

 

FixedArray fa1( 5 );               //#1 OK

FixedArray fa2 = 5;                //#2 Fehler!

FixedArray fa3 = FixedArray( 5 );  //#3 OK

 

besteht. Im ersten Fall ist eine implizite Konvertierung trotz explicit deklariertem Konstruktor möglich, im zweiten Fall dagegen nicht. Konstruktion #3 ist zulässig, da der Ausdruck FixedArray( 5 ) wieder eine explizite Konvertierung darstellt.

Beachten Sie bitte, dass

q#2 erlaubt wäre, wenn der Konstruktor nicht explicit deklariert wäre

qweder im Fall #2 noch #3 eine Zuweisung stattfindet, sondern in al­len drei Fällen der Wandlungskonstruktor für int verwendet wird.

&   Wandlung über Operatoren

Die zweite Möglichkeit, benutzerdefinierte Wandlungen implizit ab­laufen zu lassen, besteht in der Implementierung eines Wandlungs­operators. Während der Wandlungskonstruktor im Zieltyp angeordnet war (hier die Klasse B), wird der Wandlungsoperator im Quelltyp (hier die Klasse A) platziert.

 

class B;            // Deklaration

 

struct A

{

  int    i;

  double d;

 

  operator B() const;      // Wandlungsoperator

};

 

struct B

{

  string s;

};

 

Beachten Sie bitte, dass der Zieltyp B zumindest deklariert sein muss, damit die Klassendefinition von A übersetzt werden kann: schließlich wird der Name B bei der Deklaration der Operatorfunktion benötigt. Alternativ kann man natürlich die Klassendefinition von B voranstel­len:

 

struct B

{

  string s;

};

 

struct A

{

  int    i;

  double d;

 

  operator B() const;      // Wandlungsoperator

};

 

Nun kann unser Testprogramm ebenfalls ohne Syntaxfehler übersetzt werden:

 

void f( const B& );

 

void g()

{

  A a;

  f( a );           // OK – impliziter Aufruf des Wandlungsoperators

}

 

Auch in dieser Variante sind temporäre Objekte im Spiel: der Wand­lungsoperator liefert ein B-Objekt über den Stack zurück, das der Compiler irgendwo verwalten muss, um dann f eine Referenz darauf übergeben zu können.

Der Operator kann analog zur Version mit Konstruktor folgenderma­ßen implementiert werden:

 

A::operator B() const

{

  char buf[ 32 ];

  sprintf( buf, "i: %i, d: %f", i, d );

 

  B b;

  b.s = buf;

  return b;

}

 

Damit eine Operatorfunktion eine gültige Konvertierungsfunktion ist, sind einige Punkte zu beachten:

q  Die Operatorfunktion ist als Mitgliedsfunktion der Klasse des Quell­typs deklariert.

q  Die Operatorfunktion hat den gleichen Namen wie der Zieltyp.

q  Die Operatorfunktion deklariert keine Parameter und keinen Rück­gabetyp. Der Name der Operatorfunktion ist automatisch auch der Rückgabetyp.

 

Als Namen von Wandlungs-Operatorfunktionen sind alle gültigen Ty­pen er­laubt, also z.B. auch Zeigertypen, Referenzen auf andere Typen oder kon­stante Typen. Da eine Operatorfunktion ihr Objekt norma­lerweise nicht ändert, wird sie in der Regel als konstante Mitglieds­funktion deklariert.

 



[1]      d.h. mit Ausnahmen, auf die wir gleich zu sprechen kommen.

[2]      Die Standardbibliothek bietet mit den Strömen einen Ersatz, der die Nachteile ver­meidet. Eine Diskussion findet sich z.B. in [Aupperle2003], [Josuttis1999] oder [Stroustrup1999].

[3]      Eine ähnliche Situation haben wir bei der Einführung von Referenzen in Kapitel 11 (Zeiger und Referenzen) auf Seite 258 gesehen.




Vergleich der C und C++ Wandlungsoperatoren | Wandlungen über Konstruktor oder Operatorfunktion?