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 prinzipiell zwei
Möglichkeiten bereit: Konstruktoren
und Wandlungsoperatoren. In beiden
Fällen läuft eine erforderlich Wandlung prinzipiell
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 explizit aufgerufen werden muss.
Wir betrachten im folgenden die generelle Aufgabenstellung, ein Objekt
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.
Man kann für jede erforderliche Wandlung immer eine spezielle
Funktion schreiben. Um A-Objekte
in B-Objekte
zu wandeln, deklariert 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.
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 nachfolgende Parameter
eingesetzt. Die Sonderzeichen bestimmen, welcher 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ß genug für die Ausgabe ist. sprintf führt keine
Überprüfungen durch.
Was passiert, wenn der Puffer zu klein ist?
Eine weitere Problematik ist die Angabe der Platzhalter im Formatierungsstring.
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
auszugeben (und analog die int-Größe
a_in.i
mit dem Formatierer %f)
– was natürlich nicht funktioniert.
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 Situationen
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,
initialisiert 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 funktioniert nicht, da temporäre
Objekte nur an konstante Referenzen gebunden 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 Änderungen sich aber nirgendwo niederschlagen: das
temporäre Objekt verschwindet ja nach Ausführung von f wieder.
Die Änderung eines 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
}
Funktioniert
die implizite Wandlung auch dann, wenn der Konstruktor mehrere Argumente hat,
von denen alle bis auf eines mit Vorgabewerten versehen sind? Beispiel:
struct B
{
B( const A&, int i = 0 );
string s;
};
Wird die
Wandlung auch in anderen Situationen wie z.B. der Zuweisung 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?
Welche
Auswirkungen hat es, wenn das Argument im B-Konstruktor nicht konstant
ist?
struct B
{
B( A& ); // nicht konstantes Argument
string s;
};
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.
Die
implizite Wandlung über einen Konstruktor mit einem Argument ist nicht immer
erwünscht, auch wenn man auf den Konstruktor selber 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 verwendet wird.
Ein Standardfall für eine solche Situation kann an unserer Klasse FixedArray
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 FixedArray
statt, da der Zieltyp FixedArray
einen geeigneten Wandlungskonstruktor deklariert.
Was man also benötigt ist eine Möglichkeit, den Konstruktor zu behalten,
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“
Kann die Deklaration von Konstruktoren als explicit auch für die Klassen
FractInt
und Complex
sinnvoll sein?
Weiterhin interessant ist, dass bei explicit deklarierten
Konstruktoren 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 allen drei Fällen der Wandlungskonstruktor für int verwendet wird.
Die
zweite Möglichkeit, benutzerdefinierte Wandlungen implizit ablaufen zu lassen,
besteht in der Implementierung eines Wandlungsoperators.
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 voranstellen:
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 Wandlungsoperator 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 Quelltyps deklariert.
q Die Operatorfunktion hat den gleichen Namen
wie der Zieltyp.
q Die Operatorfunktion deklariert keine
Parameter und keinen Rückgabetyp. Der Name der Operatorfunktion ist
automatisch auch der Rückgabetyp.
Als Namen von Wandlungs-Operatorfunktionen sind alle gültigen Typen
erlaubt, also z.B. auch Zeigertypen, Referenzen auf andere Typen oder konstante
Typen. Da eine Operatorfunktion ihr Objekt normalerweise nicht ändert, wird
sie in der Regel als konstante Mitgliedsfunktion deklariert.