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

EXPLIZITE TYPWANDLUNGEN
 
&&

Manchmal kann es sinnvoll sein, den Schutz, den das strenge Typ­system der Sprache mit sich bringt, außer Kraft zu setzen. Schreibt man z.B. (für einen beliebigen Typ T)

 

T* pt = new T;

...

void* pv = pt;

pt = pv;            // Fehler!

 

ist die Zuweisung an pt aus Sicherheitsgründen nicht erlaubt. Weiß man allerdings wie in unserem Beispiel hier, dass pv tatsächlich auf ein T-Objekt zeigt, kann die Wandlung gefahrlos durchgeführt wer­den: allerdings muss der Programmierer dies explizit notieren – denn nur der Programmierer kann wissen, ob eine solche Wandlung in ei­ner bestimmten Situation gefahrlos möglich ist.

C++ stellt für solche und ähnliche Fälle eine Reihe von Typwand­lungsoperatoren bereit, die jeweils spezielle Aufgaben haben. Dar­über hinaus gibt es noch eine aus Kompatibilitätsgründen aus C übernommene Wandlungsmöglichkeit, die man allerdings in C++ nicht mehr verwenden sollte.

Folgende Tabelle zeigt eine Übersicht über die C++-Wandlungs­ope­ratoren[1]:

 

Operator

Bedeutung

 

 

static_cast

„Gutmütige“ (also prinzipiell gefahrlos mögliche) Wand­lungen

dynamic_cast

Wandlungen zwischen Typen in Klassenhierarchien

const_cast

Entfernen oder Hinzufügen von const und/oder vo­la­tile

reinterpret_cast

Wandelt zwischen nahezu beliebigen Typen unter Beibe­haltung des Bitmusters

 

 

Hinzu kommen die aus C übernommene Wandlungsmöglichkeiten, für die es zwei Notationen gibt:

 

Operator

Bedeutung

 

 

(T)x

Wandelt Argument x in Typ T (cast-Notation)

T(x)

dito (Funktionsnotation)

 

 

 

Alle C++-Wandlungsoperatoren verwenden die Syntax

 

operator< T >( arg );

 

Dabei wird der Wert arg in den Typ T gewandelt, sofern der jewei­lige Operator und die Situation dies zulassen. Die Wandlungen wer­den (mit Ausnahme des Operators dyna­mic_cast) statisch durchge­führt, d.h. eine unzulässige Wandlung wird bereits beim Übersetzen erkannt und führt dann zu einem Syntaxfehler. Operator dyna­mic_cast kann darüber hinaus eine dynamische Wandlung durch­führen, d.h. hier kann zusätzlich ein Fehler zur Laufzeit auftreten.

Folgendes Beispiel zeigt die prinzipielle Anwendung der Operatoren am Beispiel von static_cast:

 

double d = 2.999;

int i    = static_cast<double>( d );

 

Die Wandlung gehört zu den Standardkonvertierungen und könnte deshalb auch implizit ablaufen.

Die Konstruktion

 

int i = const_cast<double>( d );        // Fehler

 

liefert hingegen einen Syntaxfehler, da Operator const_cast nur zum Entfernen (bzw. Hinzufügen) von const oder volatile ver­wendet werden kann.

In den folgenden Abschnitten betrachten wir die einzelnen Wand­lungsmöglichkeiten genauer.

&   Operator static_cast<>

Der Operator ist für alle diejenigen Konvertierungsaufgaben vorgese­hen, die „gutmütig“ sind. Dazu gehören:

qStandardkonvertierungen (s.o.)

q  Konvertierungen vom Typ T* (für einen beliebigen Typ T) nach void* und zurück. Dabei ist sichergestellt, dass der zurückge­wandelte Zeiger den gleichen Wert hat. Nach der Ausführung der Anweisungen

 

    A* pa = new A;

    void* pv = pa;                     // implizite Wandlung

    A* pa2 = static_cast<A*>( pv );    // explizite Wandlung notwendig

 

     haben a und a2 den gleichen Wert, zeigen also auf das gleiche Ob­jekt. Wird die Wandlung in einen anderen Zeigertyp durchge­führt, ist das Ergebnis undefiniert:

 

    B* pb = static_cast<B*>( pv );    // undefiniertes Verhalten

 

q  Konvertierung von einem integralen Typ zu einem Aufzählungs­typ. Bei­spiel:

 

    enum Colors { red, yellow, blue };

 

    int i = 1;

    Colors c = static_cast<Colors>( i );

 

     c hat den Wert yellow erhalten[2].  Gehört der Quellwert nicht zu den Konstanten der Aufzählung, ist das Verhalten undefiniert:

 

    Colors c = static_cast<Colors>( 99 );  // undefiniertes Verhalten

 

q  Konvertierungen in Klassenhierarchien, wenn diese statisch (also ohne Kenntnis des dynamischen Typs eines Zeigers) durchgeführt werden können[3].

 

Der wichtige Punkt bei den Konvertierungen mit static_cast ist also, dass prinzipiell nicht alle Werte des Quelltyps auch gültige Werte des Zieltyps sein müssen – aus diesem Grunde laufen die Konvertierun­gen ja auch nicht implizit ab. Der Programmierer muss also wissen, was er tut. Tut er das Richtige, sind Wandlungen mit static_cast wohldefiniert, ansonsten undefiniert. Welche der beiden Möglichkei­ten vorliegt, hängt vom konkreten Wert ab, der gewandelt werden soll.

Es stellt sich die Frage, warum Standardkonvertierungen in obiger Liste auftauchen. Standardkonvertierungen können ja implizit ablau­fen und brauchen keine explizite Wandlung. Anstelle von

 

void f( int i_in )

{

  double d = static_cast<double>( i_in );   // explizite Wandlung

  /* ... */

}

 

kann man direkt

 

void f( int i_in )

{

  double d = i_in;                        // implizite Wandlung

  /* ... */

}

 

schreiben. Beachten Sie jedoch folgenden Fall:

 

void g( int );     // #1

void g( double );  // #2

 

void f( int i_in )

{

  g( static_cast<double>( i_in ));  // ruft #2

}

 

Hier möchte der Programmierer die Version #2 der Funktion g für double aufrufen, der Aktualparameter ist jedoch vom Typ int. Da auch eine Variante der Funktion g für int existiert, ist eine explizite Typwandlung erforderlich.

Beachten Sie bitte, dass das Entfernen von const (const-cast-away) nicht als „gutmütige“ Kon­vertierung gilt. Die Konvertierung in

 

void f( const A* pa )

{

  A* pa2 = static_cast<A*>( pa );              // Fehler!

  /* ... */

}

resultiert in einem Syntaxfehler. Zum Entfernen von const ist aus­schließlich Operator const_cast vorgesehen!

Gleiches gilt für die Konvertierung beliebiger Zeigertypen. Es ist aus­drück­lich nicht erlaubt, etwas wie

 

struct A {};

struct B {};

 

A* pa = new A;

B* pb = static_cast<B*>( pa );          // Fehler!

 

zu schreiben! Eine solche Konstruktion ist nahezu immer ein Fehler. Es gibt keinen Grund, einen Zeiger vom Typ A* auf ein Objekt eines ganz anderen Typs B zeigen zu lassen.

Erlaubt ist allerdings die Wandlung von A* nach void* und von dort aus (mit Hilfe von static_cast) nach B*. Dies ist natürlich genauso ein Fehler wie direkt von A* nach B*. Die Konstruktion ist aber nicht zu verhindern, wenn man die manchmal sinnvollen Wandlungen von A* nach void* und wieder zurück nach A* nicht gänzlich verbieten will.

Ganz allgemein lässt sich sagen, dass eine Konvertierung mit sta­tic_cast zwischen zwei Typen A und B genau dann möglich ist, wenn

qdie Wandlung eine Standardkonvertierung ist (s.o.)

qdie umgekehrte Wandlung eine Standardkonvertierung ist.

     Ist also die Anweisungsfolge

 

A a;

B b;

b = a;                  // Annahme: Standardkonvertierung

 

     möglich, ist auch

 

a = static_cast<A>( b );       // OK

 

     korrekt. Die Typen A und B sollen hierbei fundamentale Typen sein, d.h. nicht etwa Klassen mit Konstruktoren oder Wandlungs­operatoren – diese Fälle besprechen wir später in diesem Kapitel.

Betrachten wir hierzu noch einmal die Wandlungsmöglichkeiten zwi­schen Aufzählungen und integralen Typen. Die Wandlung von einem Aufzählungstyp zu einem int ist eine Standardkonvertierung und läuft deshalb implizit ab:

 

enum Colors { red, yellow, blue };

Color c = yellow;

 

int i = c;                 // OK – implizite Wandlung

 

Daher lässt sich die umgekehrte Richtung mit Hilfe von static_cast durchführen:

 

c = static_cast<Colors>( i );     // OK – explizite Wandlung

 

Wie immer bei expliziten Typwandlungen ist es die Aufgabe des Pro­grammierers, vernünftiges Verhalten sicherzustellen. Das folgende Programmsegment ist daher syntaktisch korrekt, liefert aber undefi­niertes Verhalten:

 

int i = 299;

c = static_cast<Colors>( i );     // OK – aber undefiniertes Verhalten

 

Übung 24-1:

Schreiben Sie Code, der durch eine Abfrage des Wertes von i prüft, ob die Wandlung in den Typ Colors zulässig ist.

 

&   Operator dynamic_cast

Operator dynamic_cast wird ausschließlich in Klassenhierarchien verwen­det. Polymorphismus erfordert manchmal bestimmte Konver­tierungen, die aber auch gefährlich sein können. Operator dyna­mic_cast erlaubt eine Ent­scheidung zur Laufzeit, ob eine Konvertie­rung möglich ist oder nicht.

Da der Operator ausschließlich in polymorphen Programmen im Zu­sam­menhang mit der Typidentifizierung zur Laufzeit (run time type identifica­tion, RTTI) verwendet wird, verschieben wir die Diskussion dieses Operators bis zum Kapitel 34 (Typinformationen zur Laufzeit).

&   Operator const_cast

Operator const_cast kann ausschließlich dazu verwendet werden, die const- oder volatile-Eigenschaft zu einem Typ hinzuzufügen oder wegzu­nehmen. Beispiel:

 

void f( const A* pa )

{

  A* pa2 = const_cast<A*>( pa );        // OK

  /* ... */

}

 

Hier wird ein Objekt, das eigentlich innerhalb von f nicht verändert werden soll, über a2 doch veränderbar. Es ist klar, dass dieses „Weg­casten“ von const(const-cast-away) auf Einzelfälle beschränkt blei­ben soll. In modernem C++ kann man durch Deklaration von mutable oft auf den const-cast-away verzichten.

Beachten Sie bitte, dass

qdas Entfernen von const nur dann korrekt ist, wenn das eigentli­che Objekt selber nicht konstant ist  (kein first level const), etwa wie in diesem Beispiel:

 

A a;

f( &a );         // OK

 

     Das Verhalten dieses Programmsegments ist dagegen undefiniert:

 

const A a;              // a ist konstant

f( &a );                // syntaktisch OK, aber undefiniertes Verhalten

 

     Hier ist das Objekt a tatsächlich konstant und kann deshalb nicht als nicht-konstantes Objekt behandelt werden.

qder Operator prinzipiell auch zum Zufügen von const verwendet werden kann:

 

void f( const A* pa )

{

  A* pa2           = const_cast<A*>( pa );

  const A* pa3     = const_cast<const A*>( pa2 ); // Zufügen von const

  /* ... */

}

 

Analoges gilt für das Hinzufügen bzw. Wegnehmen von volatile.

&   Operator reinterpret_cast

Operator reinterpret_cast führt keine Wandlung im eigentlichen Sinne aus, sondern interpretiert den durch eine Variable gegebenen Speicherbereich mit einem anderen Typ. Für die Praxis wichtig ist, dass dabei das Bitmuster des Speicherbereiches unverändert bleibt.

Typische Anwen­dungsfälle sind „harte“ Konvertierungen wie in

 

struct A { /*...*/ };

struct B { /*...*/ };

 

A* pa;

B* pb = reinterpret_cast<B*>( pa );  // syntaktisch OK. Bedeutung?

 

Das Ergebnis ist aus Sicht der Sprache undefiniert, es ist allein Sache des Programmierers, das Layout der Klassen A und B so zu gestalten, dass die Konvertierung ihren (zweifelhaften) Sinn macht[4].

Eine häufige Anwendung des Operators ist die Wandlung von Zei­gertypen in numerische Typen und umgekehrt. Unter der Vorausset­zung, dass Zeigertypen die gleiche Größe wie z.B. ints haben, kann man für einen beliebigen Typ T die Zeilen

 

T* pt = new T;

int val = reinterpret_cast<int>( pt );

...

T* pt2 = reinterpret_cast<T*>( val );

 

schreiben. Dadurch, dass reinterpret_cast das Bitmuster unver­ändert lässt, ist sichergestellt, dass pt2 nach der Rückwandlung den gleichen Wert wie pt besitzt.

Beachten Sie bitte, dass über den Wert von val keine Annahmen ge­troffen werden können – ausschließlich die Rückwandlung in den Originaltyp ergibt definiertes Verhalten. Insbesondere ist nicht sicher­gestellt, dass der Nullzeiger das gleiche Bitmuster wie die Zahl 0 be­sitzt. Schreibt man also

 

pt = 0;

int val = reinterpret_cast<int>( pt );

 

if ( val == 0 ) ...               // OK – aber ergibt nicht unbedingt true

 

ist nicht unbedingt sichergestellt, dass val den Wert 0 hat.

 

Übung 24-2:

Schreiben Sie Code, der überprüft, ob Zeigertypen die gleiche Größe wie ints haben. Schreiben Sie eine Funktion, die einen beliebigen Zeigertyp in ein int wandelt, vorher jedoch die Prüfung durchführt. Ist die umgekehrte Funktion (d.h. die Wandlung eines int in einen beliebigen Zeigertyp) ebenfalls möglich?

 

Die Wirkung von reinterpret_cast ist immer implementierungs­abhängig und damit nicht unbedingt portabel. Die Anwendung sollte auf Ausnahmefälle be­schränkt bleiben.


Beachten Sie bitte, dass auch reinterpret_cast nicht zum Entfer­nen von const verwendet werden kann. Die folgenden Anweisungen führen zu ei­nem Syntaxfehler:

 

void f( const A* pa )

{

  A* pa2 = reinterpret_cast<A*>( pa );         // Fehler!

  /* ... */

}

 

&   C- Wandlungsoperator

Zusätzlich zu den neuen C++-Wandlungsoperatoren gibt es die aus C geerbten Wandlungsmöglichkeiten. Für den C-Wandlungsoperator gibt es zwei Notationen:

qdie Funktionsnotation T(x)

qdie cast-Notation (T)x

 

Beide Notationen sind funktional identisch: sie wandeln den Wert x in einen Wert vom Typ T, jedoch gibt es Unterschiede im Aufruf, die durch die Syntax bestimmt sind.

Die Wandlung in Funktionsnotation ist nur möglich, wenn T ein Typ ist, der sich in einem Wort schreiben lässt. Konstruktionen wie z.B. für eine Klasse A:

 

void f( void* p_in)

{

  A* ap = A*(p_in);               // Fehler!

}

 

sind nicht erlaubt. Hier muss die cast-Notation verwendet werden:

 

void f( void* p_in )

{

  A* ap = (A*)p_in;               // OK

}

 

Der Typ selber kann durchaus ein zusammen gesetzter Typ sein – dann muss allerdings ein typedef verwendet werden. Folgendes ist zulässig:

 

tyedef A* AP;

void f( void* p_in )

{

  A* ap = (AP)p_in;               // OK

}

 

Demgegenüber kann die Funktionsnotation auch mehr als einen Pa­rameter wandeln. Notationen wie

 

A a;

a = A( 1, 2 );

 

sind möglich. Jedoch benötigt die Klasse A einen Konstruktor, der mit zwei ints aufgerufen werden kann. Hierbei handelt es sich um eine benutzerdefinierte Wandlung mit Klassen, die wir später in diesem Kapitel besprechen werden.

Der C-Wandlungsoperator führt je nach Kontext die Funktionalität von static_cast, dynamic_cast, const_cast oder rein­terpret_cast durch.

 

Übung 24-3:

Welche Wandlung führt der C-Wandlungsoperator in obigen Beispie­len durch? Schreiben Sie die Beispiele mit dem passenden C++-Wand­lungsoperator.

 

Übung 24-4:

Welche Wandlung führt der C-Wandlungsoperator in diesem Beispiel durch? Schreiben Sie das Beispiel mit dem passenden C++-Wand­lungsoperator.

 

enum Colors { red, yellow, blue };

 

f( int i_in )

{

  Colors c = Colors( i_in ); // Wandlung in Funktionsnotation

  ...

}

 

 

 



[1]      Wir bezeichnen die Operatoren als „C++-Wandlungsoperatoren“ um sie von den aus C geerbten Wandlungsoperatoren in Funktionsnotation zu unterscheiden.

[2]       Einige ältere Compiler lassen die Wandlung von einem integralen Typ zu einem Aufzählungstyp implizit zu. Dies ist nach dem Standard nicht erlaubt und sollte auch mit einem solchen Compiler vermieden werden, da künftige Compilerver­sionen sicher einen Fehler melden werden.

[3]       Klassenhierarchien, den dynamischen und statischen Typ von Zeigern behan­deln wir ab Kapitel 31  (Virtuelle Funktionen).

[4]      Dies kann z.B. der Fall sein, wenn A und B (zumindest am Anfang) die gleiche Anordnung von Mitgliedern haben. Dadurch ist das Speicherlayout der beiden Klassen soweit identisch, dass man mit A*-Zeigern auf Teile von B-Objekten zugreifen kann. In reinen C++-Programmen ist dies extrem schlechter Stil, ist aber manchmal beim Einbinden von C-Anteilen unvermeidlich.




Zeiger und Referenzen | Vergleich der C und C++ Wandlungsoperatoren