Skip to content

Latest commit

 

History

History
444 lines (320 loc) · 13.4 KB

File metadata and controls

444 lines (320 loc) · 13.4 KB

Typumwandlungen

Zurück


Quellcode


Inhalt


Einleitung

Die Programmiersprache C++ kennt eine ganze Reihe verschiedener Typkonvertierungsmöglichkeiten. Einen Überblick über die expliziten Typkonvertierungen finden Sie in Abbildung 1 vor:

Abbildung 1: Überblick über die unterschiedlichen Typkonvertierungsmöglichkeiten in C/C++


Implizite Konvertierungen

Eine implizite Konvertierung wird vom Compiler automatisch durchgeführt, wenn ein Ausdruck in einen seiner kompatiblen Typen konvertiert werden muss.

Beispielsweise können Konvertierungen zwischen primitiven Datentypen implizit erfolgen.

01: long a = 5;      // int implicitly converted to long 
02: double b = 123l; // long implicitly converted to double

Maschinencode:

                long a = 5;      // int implicitly converted to long 
00007FF67026C74A  mov         dword ptr [rbp+44h],5  
                double b = 123l; // long implicitly converted to double
00007FF67026C751  movsd       xmm0,mmword ptr [__real@405ec00000000000 (07FF670503070h)]  
00007FF67026C759  movsd       mmword ptr [rbp+68h],xmm0

Implizite primitive Konvertierungen können auch zur Laufzeit durchgeführt werden:

01: int a = 123;    
02: long b = 123l; 
03: 
04: long n = a;      // int implicitly converted to long 
05: double m = b;    // long implicitly converted to double

Maschinencode:

                int a = 5;    
00007FF67026C75E  mov         dword ptr [rbp+84h],5  
                long b = 123l; 
00007FF67026C768  mov         dword ptr [rbp+0A4h],7Bh  

                // implicit conversions, done by the machine code
                long n = a;      // int implicitly converted to long 
00007FF67026C772  mov         eax,dword ptr [rbp+84h]  
00007FF67026C778  mov         dword ptr [rbp+0C4h],eax  
                double m = b; // long implicitly converted to double
00007FF67026C77E  cvtsi2sd    xmm0,dword ptr [rbp+0A4h]  
00007FF67026C786  movsd       mmword ptr [rbp+0E8h],xmm0

Hier erkennen wir, dass beispielsweise bei der Konvertierung von ganzzahligen Werten nach Gleitpunktwerten Konvertierungsroutinen zur Laufzeit ausgeführt werden müssen. Dies kostet Performanz!

Diese impliziten primitiven Konvertierungen lassen sich weiter in zwei Arten unterteilen: Promotion und Demotion (in etwa „Heraufstufung” und „Herabstufung”).

Eine Promotion erfolgt, wenn ein Ausdruck implizit in einen größeren Typ konvertiert wird, und eine Demotion erfolgt, wenn ein Ausdruck in einen kleineren Typ konvertiert wird.

01: // Promotion
02: long   a = 123;   // int promoted to long
03: double b = 123l;  // long promoted to double

Da eine Demotion zu Informationsverlust führen kann, erzeugen diese Konvertierungen bei den meisten Compilern eine Warnung:

01: // Demotion
02: int  a = 10.0;   // warning: possible loss of data
03: bool b = 123;    // warning: possible loss of data

Output-Window:

warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data
warning C4305: 'initializing': truncation from 'int' to 'bool'

Wenn der potenzielle Informationsverlust beabsichtigt ist, kann man die Warnung durch eine „explizite” Konvertierung unterdrücken.

Dies führt uns zum nächsten Abschnitt:

Explizite Konvertierungen: C-Style Cast

Die erste explizite Konvertierung (im engl. „Type Cast”) ist aus C übernommen und wird allgemein als C-Style-Cast bezeichnet.

Der gewünschte Datentyp wird einfach in Klammern links neben dem zu konvertierenden Ausdruck platziert:

01: // C-style cast
02: int  a = (int) 123.456;  // double demoted to int
03: bool b = (bool) 123;     // int demoted to bool

Der C-Style-Cast eignet sich für die meisten Konvertierungen zwischen primitiven Datentypen.

Bei Konvertierungen zwischen Klassen und Zeigern kann er jedoch zu mächtig sein.

Um eine bessere Kontrolle über die verschiedenen möglichen Konvertierungsarten zu erhalten, wurden in C++ vier neue Cast-Operationen eingeführt, die sogenannten Named Casts oder New-Style-Casts.

Explizite C++ Konvertierungen

Die Namen der vier New-Style-Casts lauten

static_cast<new_type> (expression) 
reinterpret_cast<new_type> (expression) 
const_cast<new_type> (expression) 
dynamic_cast<new_type> (expression)

Wie zu sehen ist, folgt auf den Namen des Casts der neue Typ in spitzen Klammern und anschließend der zu konvertierende Ausdruck in Klammern.

Diese Casts ermöglichen eine präzisere Kontrolle über die Konvertierung, was wiederum dem Compiler das Erkennen von Konvertierungsfehlern erleichtert.

Bemerkung:
Der C-Style-Cast umfasst in seiner Definition den static_cast, den reinterpret_cast und den const_cast. Bei falscher Anwendung führt dieser Cast daher häufiger zu subtilen Konvertierungsfehlern.

Die static_cast-Operation

Die static_cast-Operation führt Konvertierungen zwischen kompatiblen Typen durch.

Er ähnelt dem C-Style-Cast, ist aber restriktiver.

Beispielsweise erlaubt der C-Style-Cast, dass ein Integer-Zeiger auf eine char-Variable zeigt:

// C-Style-Cast
char c = 10;       // 1 byte
int* p = (int*)&c; // 4 bytes, compiles, works ?!?

Da dies zu einem 4-Byte-Zeiger führt, der auf 1 Byte allokierten Speicher verweist, führt das Schreiben in diesen Zeiger entweder zu einem Laufzeitfehler oder überschreibt angrenzenden Speicher:

*p = 5;            // run-time error: stack corruption

Im Gegensatz zum Cast im C-Style ermöglicht der statische Cast dem Compiler zu überprüfen, ob die Pointer- und Pointee-Datentypen kompatibel sind, wodurch der Programmierer diese falsche Zeigerzuweisung während der Kompilierung erkennen kann:

01: // static_cast
02: char c = 10;                    // 1 byte
03: int* p = static_cast<int*>(&c); // 4 bytes

Der Compiler reagiert mit der Fehlermeldung Invalid type conversion: 'static_cast': cannot convert from 'char *' to 'int *'.

Die reinterpret_cast-Operation

Um eine Zeigerkonvertierung zu erzwingen, verwendet man stattdessen die reinterpret_cast-Operation. Diese arbeitet im Hintergrund auf dieselbe Weise wie der C-Style-Cast:

01: // reinterpret_cast
02: char c = 10;                           // 1 byte
03: int* p = reinterpret_cast<int*>(&c);   // 4 bytes // compiles

Die reinterpret_cast-Operation verarbeitet Konvertierungen zwischen bestimmten, nicht verwandten Typen, beispielsweise von einem Zeigertyp in einen anderen, inkompatiblen Zeigertyp.

Er führt lediglich eine binäre Kopie der Daten durch, ohne das zugrunde liegende Bitmuster zu verändern oder zu interpretieren.

Beachten Sie, dass das Ergebnis einer solchen Low-Level-Operation systemspezifisch und daher nicht portierbar ist.

Diese Cast-Operation sollte mit Vorsicht verwendet werden, wenn sie sich nicht vollständig vermeiden lässt.

Ein zweites Beispiel:

01: struct MyStruct
02: {
03:     char x1;
04:     char x2;
05:     char x3;
06:     char x4;
07: };
08: 
09: {
10:     struct MyStruct s{ 'A' , 'B' , 'C' , '\0' };
11: 
12:     // reinterpret struct as char* pointer :-) 
13:     char* ptr = reinterpret_cast<char*>(&s);
14: 
15:     std::println("{}", ptr);
16: }

Ausgabe:

ABC

Die const_cast-Operation

Der dritte C++-Cast ist die const_cast-Operation. Sie wird hauptsächlich verwendet, um den const-Modifikator einer Variablen hinzuzufügen oder zu entfernen.

01: const int constVar = 123;
02: int* nonConstIp = const_cast<int*>(&constVar); // removes const

Obwohl die const_cast-Operation die Änderung des Wertes einer Konstanten ermöglicht, handelt es sich dabei immer noch um ungültigen Code, der einen Laufzeitfehler verursachen kann.

Dies könnte beispielsweise der Fall sein, wenn sich die Konstante in einem Bereich des Read-Only-Speichers befindet:

*nonConstIp = 10; // potential run-time error

Die const_cast-Operation wird häufig dann verwendet, wenn eine Funktion ein nicht konstantes Zeigerargument annimmt, obwohl sie den Zeiger nicht ändert:

01: void print(int* ptr)
02: {
03:     std::println("{}", *ptr);
04: }

Der Funktion kann dann mit Hilfe einer const_cast-Operation eine konstante Variable übergeben werden:

01: const int constVar = 123;
02: 
03: // print(&constVar);                  // error: cannot convert argument 1 from 'const int*' to 'int*'
04: 
05: print(const_cast<int*>(&constVar));   // compiles, runs

C-Style-Casts und New-Style-Casts

Beachten Sie, dass der C-Style-Cast den const-Modifikator ebenfalls entfernen kann. Da diese Konvertierung jedoch im Hintergrund erfolgt, sind New-Style-Casts vorzuziehen.

Ein weiterer Grund für die Verwendung von New-Style-Casts ist, dass sie im Quellcode leichter zu finden sind als ein C-Style-Cast.

Das ist wichtig, da Typkonvertierungsfehler schwer zu entdecken sein können.

Ein dritter Grund für die Verwendung von C-Style-Casts ist deren unkomfortable Schreibweise.

Da explizite Konvertierungen in vielen Fällen vermieden werden können, wurde dies bewusst so gewählt, dass Entwickler nach einer anderen Lösung suchen.


Die dynamic_cast-Operation

Im folgenden Beispiel wird ein Derived-Zeiger mithilfe einer dynamischen Konvertierung in einen Base-Zeiger umgewandelt.

Diese „derived-to-base” Konvertierung ist erfolgreich, da das Derived-Objekt ein vollständiges Base-Objekt enthält:

01: class Base
02: {
03: public: 
04:     virtual void test() { std::println("Base"); }
05: };
06:             
07: class Derived : public Base
08: {
09: public:
10:     void test() override { std::println("Derived"); }
11: };
12: 
13: {
14:     Derived* child = new Derived();
15:     Base* base = dynamic_cast<Base*>(child); // ok
16:     base->test();
17: }

Ausgabe:

Derived

Das nächste Beispiel versucht, einen Base-Zeiger in einen Derived-Zeiger umzuwandeln. Da das Base-Objekt kein vollständiges Derived-Objekt enthält, schlägt diese Zeigerkonvertierung fehl.

Um dies anzuzeigen, gibt der dynamische Cast einen Nullzeiger zurück. So lässt sich bequem zur Laufzeit überprüfen, ob eine Konvertierung erfolgreich war:

01: Base* base = new Base();
02: Derived* child = dynamic_cast<Derived*>(base);
03: 
04: if (child == 0) {
05:     std::println("Null pointer returned!");
06: }

Ausgabe:

Null pointer returned!

Wird anstelle eines Zeigers eine Referenz konvertiert, schlägt die dynamische Umwandlung mit einer std::bad_cast-Exception fehl. Dies muss mithilfe einer try-catch-Anweisung behandelt werden:

01: Base base;
02: 
03: try {
04:     Derived& child = dynamic_cast<Derived&>(base);
05: }
06: catch (std::bad_cast& e)
07: {
08:     std::println("{}", e.what());
09: }

Ausgabe:

Bad dynamic_cast!

static_cast- oder dynamic_cast-Operation

Der Vorteil eines dynamischen Casts besteht darin, dass der Programmierer während der Laufzeit prüfen kann, ob eine Konvertierung erfolgreich war.

Der Nachteil ist der damit verbundene Performance-Overhead.

Aus diesem Grund wäre im ersten Beispiel ein statischer Cast vorzuziehen gewesen, da eine „derived-to-base” Konvertierung nie fehlschlagen kann:

01: // less performance overhead : using a static_cast
02: Derived* child = new Derived();
03: Base* base = static_cast<Base*>(child); // ok
04: base->test();

Ausgabe:

Derived

Wäre die Konvertierung von der Basis- zur abgeleiteten Klasse mit einem statischen anstatt einem dynamischen Cast durchgeführt worden, wäre die Konvertierung nicht fehlgeschlagen.

01: Base* base = new Base();  // toggle between Base and Derived
02: Derived* child = static_cast<Derived*>(base);
03: 
04: if (child == 0) {
05:     std::println("Null pointer returned!");
06: }
07: else {
08:     std::println("static_cast successful!");   // Oooops
09: }

Ausgabe:

static_cast successful!

Die Konvertierung hat einen Zeiger zurückgegeben, der auf ein unvollständiges Objekt verweist! Die Dereferenzierung eines solchen Zeigers kann zu Laufzeitfehlern führen.


Literatur

Eine sehr gute Beschreibung aller möglichen Tpykonvertierungsmöglichkeiten findet sich hier:

Tutorial Type Conversions
(abgerufen am 23.3.2025).

Haben Sie sich schon einmal gefragt, warum C-Style-Casts und reinterpret_cast-Casts als schädlich gelten? In diesem Artikel wird genauer betrachtet, was bei ihnen schiefläuft:

C++ Background: Static, reinterpret and C-Style casts
(abgerufen am 23.3.2025).


Zurück