C++ Kurs

Laufzeit-Typinformationen

Die Themen:

Einleitung
RTTI
Beispiel und Übung

Einleitung

In manchen Fällen kann es sehr hilfreich sein, Informationen über den Datentyp eines Objekts (oder auch einer einfachen Variablen) zu erhalten. Hierzu bietet C++ die Typinformation zur Laufzeit (Runtime type information = RTTI) an. Wie und für was diese Typinformation eingesetzt werden kann, das soll nun wieder anhand eines Beispiels erläutert werden.

 

RTTI

Das Ausgangsbeispiel

Unten sehen die bekannten Klassen GBase, als Basisklasse für alle Grafikobjekte, sowie die beiden von GBase abgeleiteten Klassen Circle und Bar für die eigentlichen Grafikobjekte. In main() wird dann ein Feld vom Typ Zeiger auf Basisklasse definiert, in dem die Zeiger auf die Grafikobjekte abgelegt werden.


// Klassendefinitionen
class GBase
{....};
class Circle: public GBase
{....};
class Bar: public GBase
{....};
// main() Funktion
int main()
{
    // Basiszeigerfeld definieren
    GBase *pGraphic[3];
    // 3 Grafikobjekte im Feld ablegen
    pGraphic[0] = new Bar(...);
    pGraphic[1] = new Circle(...);
    pGraphic[2] = new Bar(...);
    ....
    // Objekte hier wieder löschen
}

Ich hoffe Sie erinnern sich noch daran, dass in einem Basisklassenzeiger auch Zeiger auf abgeleitete Objekte abgelegt werden können?!

Soweit nicht Ungewöhnliches.

Wir erweitern jetzt die Basisklasse um eine Memberfunktion Align(...) die es uns gestatten soll, zwei Grafikobjekte auszurichten. Die Ausrichtung soll der Einfachheit halber in der Art erfolgen, dass die X-Positionen gleichgesetzt werden.


void GBase::Align(GBase *pGObj)
{
    pGObj->xPos = xPos; // xPos gleichsetzen
}
// Mögliche Aufrufe
pGraphic[0]->Align(pGraphic[1]);
pGraphic[0]->Align(pGraphic[2]);

Unterhalb der Definition der Memberfunktionen sehen Sie zwei mögliche Aufrufe der neuen Memberfunktion. Dort werden die Grafikobjekte zwei und drei auf die gleiche X-Position wie das erste Grafikobjekt ausgerichtet.

Diese neue Memberfunktion Align(...) wollen wir gleich so abändern, dass nur noch gleiche Grafiken ausgerichtet werden können, d.h. Sie können zwei Bar oder zwei Circle Objekte ausrichten aber nicht mehr ein Bar Objekt an einem Circle Objekt. Und hier kommt die RTTI ins Spiel.

Die Runtime Typeinformation (RTTI)

Um zur Laufzeit feststellen zu können, ob die auszurichtenden Objekte vom gleichen Typ sind, muss die Memberfunktion Align(...) Informationen über die Typen (RTTI) der auszurichtenden Objekt erhalten. Damit ein Objekt aber eine RTTI enthält, muss dessen Klasse polymorph sein, d.h. sie muss mindestens eine virtuelle Memberfunktion besitzen. Im Zusammenhang mit virtuellen Memberfunktionen erinnern Sie sich vielleicht noch daran, dass eine in einer Basisklasse als virtuelle deklarierte Memberfunktion in allen abgeleiteten Klassen automatisch ebenfalls virtuell ist. Wenn Sie also für eine Klasse RTTI benötigen, muss entweder die Klasse selbst oder aber eine ihrer Basisklassen mindestens eine virtuelle Memberfunktion enthalten.

Aber sehen wir uns jetzt die RTTI an. Um die RTTI einsetzen zu können, muss die Datei <typeinfo> mittels include-Anweisung zunächst eingebunden werden. Diese Datei enthält die Definition der für die RTTI benötigten Struktur type_info.

Um die Typinformation eines Objekts letztendlich zu erhalten, rufen Sie den Operator

type_info& typeid(DTYP);

auf. DTYP ist die Variable oder das Objekt, dessen Typinformation bestimmt werden soll. Für DTYP kann aber auch ein bestimmter Datentyp oder Klassenname eingesetzt werden. Als Ergebnis liefert der Operand eine Referenz auf die Struktur type_info, die die Typinformation enthält. Sie müssen diese Struktur nicht selbst auswerten, sondern hierfür stehen verschiedene überladene Operatoren bereit, die in der nachfolgenden Tabelle aufgeführt sind.

Operator Bedeutung
bool operator == (const type_info& COp2) Vergleicht ob zwei Datentypen identisch sind
bool operator != (const type_info& COp2) Vergleicht ob zwei Datentypen unterschiedlich sind
bool before(const type_info& COp2) Funktion für interne Zwecke
const char* name() Liefert einen Zeiger auf den Datentyp als ASCII String; der String selbst ist aber nicht standardisiert, so dass unterschiedliche Compiler unterschiedlich Strings liefern können.

Somit können Sie z.B. mit folgender Anweisung vergleichen, ob das Objekt anyGraph zur Klasse Bar gehört:

if (typeid(anyGraph) == typeid(Bar))
    ....

Vergleichen von Datentypen

Und damit lässt sich die Memberfunktion Align(...) der Basisklasse nun wie angegeben definieren.


void GBase::Align(GBase* pGObj)
{
    // Grafiktypen vergleichen
    if (typeid(*this) != typeid(*pGObj))
    {
        // Typen ungleich, Meldung ausgeben und Exception auslösen
        cout << "Fehler beim Ausrichten " << typeid(*this).name();
        cout << '-' << typeid(*pGObj).name()<< endl;
        throw "Ungleiche Grafikelemente!";
    }
    // Gleiche Grafiktypen, dann ausrichten
    pGObj->xPos = xPos;
    pGObj->yPos = yPos;
}

Die Memberfunktion Align(...) erhält als Parameter einen Zeiger auf das auszurichtende Objekt. In der Memberfunktion wird zuerst der Datentyp des aktuellen Objekts typeid(*this) mit dem des auszurichtenden Objekts typeid(*pGObj) verglichen. Beachten Sie hier bitte, dass die Zeiger dereferenziert werden müssen, da Sie ja den Datentyp des Objekts und nicht den des Zeigers ermitteln wollen. Der Datentyp des Zeigers ist hier immer GBase*! Sind die Datentypen unterschiedlich, so werden die Namen der beiden Datentypen ausgegeben (Aufruf der Memberfunktion name( ) ) und eine Exception ausgelöst. Bei gleichen Datentyp werden die Objekte dann entsprechend der Vorgabe ausgerichtet.

Nachfolgend sehen Sie ein Beispiel für die Ausgabe der Memberfunktion, wenn versucht wird ein Circle Objekt an einem Bar Objekt auszurichten.

Fehler beim Ausrichten class Bar-class Circle

Das Vergleichen von Datentypen funktioniert übrigens auch dann, wenn die Objekte über Klassen-Templates erstellt wurden.

Beispiel und Übung

Beispiel:

Das Beispiel zeigt den vollständigen Quellcode zu den vorherigen Beispielen. In main() werden drei Grafikobjekte erstellt, zwei Rechtecke und ein Kreis. Anschließend wird versucht, das zweite Rechteck und den Kreis am ersten Rechteck auszurichten. Da die Memberfunktion Align(...) der Basisklasse GBase nur die Ausrichtung von gleichen Grafikobjekten zulässt, führt die Ausrichtung des Kreises zu einer Exception.

-- Zeichne Rechteck ---
Position: 10,10
Grösse: 100,100
-- Zeichne Rechteck ---
Position: 30,20
Grösse: 300,200
-- Zeichne Kreis ---
Position: 50,50
Radius: 100
Fehler beim Ausrichten class Bar-class Circle
*** Exception: Ungleiche Grafikelemente!***
-- Zeichne Rechteck ---
Position: 10,10
Grösse: 100,100
-- Zeichne Rechteck ---
Position: 10,10
Grösse: 300,200
-- Zeichne Kreis ---
Position: 50,50
Radius: 100


// Beispiel zu RTTI

// Zuerst Dateien einbinden
#include <iostream>
#include <typeinfo>

using std::cout;
using std::endl;

// Definition der Klasse GBase
// GBase dient als Basisklasse für die Grafikelemente Bar und Circle
// HINWEIS: Damit die Runtime-Typinformation eingebunden wird
// muss die Klasse polymorph sein, d.h. mindestens eine
// virtuelle Memberfunktion besitzen!
class GBase
{
    short   xPos, yPos;       // Position der Grafik
public:
    GBase(short x, short y);
    virtual void Draw() const;
    void Align(GBase&);
};
// Definition der Memberfunktionen
// Konstruktor
GBase::GBase(short x, short y)
{
    xPos = x; yPos = y;
}
// Ausgabe der Koordinaten
void GBase::Draw() const
{
    cout << "Position: " << xPos << ',' << yPos << endl;
}
// Memberfunktion Align() der Klasse GBase
// Richtet zwei gleiche Grafikelemente aus
// Wird versucht zwei verschiedene Grafikelemente auszurichten,
// so wird eine Ausnahme ausgelöst
void GBase::Align(GBase& pGraphic)
{
    // Grafiktypen vergleichen
    if (typeid(*this) != typeid(pGraphic))
    {
        // Ungleich, Meldung ausgeben und Ausnahme auslösen
        cout << "Fehler beim Ausrichten " << typeid(*this).name();
        cout << '-' << typeid(pGraphic).name() << endl;
        throw "Ungleiche Grafikelemente!";
    }
    // Gleiche Grafiktypen, dann ausrichten
    pGraphic.xPos = xPos;
    pGraphic.yPos = xPos;
}

// Definition der Klasse Bar
class Bar: public GBase
{
    short   width, height;
  public:
    Bar(short, short, short, short);
    void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
Bar::Bar(short x, short y, short w, short h):
    GBase(x, y)
{
    width = w; height = h;
}
// Gibt die Daten des Rechtecks aus
void Bar::Draw() const
{
    cout << "-- Zeichne Rechteck ---\n";
    GBase::Draw();
    cout << "Grösse: " << width << "," << height << endl;
}

// Definition der Klasse Circle
class Circle: public GBase
{
    short   radius;
  public:
    Circle(short, short, short);
    void Draw() const;
};
// Definition der Memberfunktionen
Circle::Circle(short x, short y, short r):
    GBase(x, y)
{
    radius = r;
}
// Gibt die Daten des Kreises aus
void Circle::Draw() const
{
    cout << "-- Zeichne Kreis ---\n";
    GBase::Draw();
    cout << "Radius: " << radius << endl;
}

// main() Funktion
int main()
{
    // 3 Grafikobjekte anlegen
    GBase* pGBase1 = new Bar(10,10, 100, 100);
    GBase* pGBase2 = new Bar(30,20, 300, 200);
    GBase* pGBase3 = new Circle(50,50,100);

    // Grafiken ausgeben
    pGBase1->Draw();
    pGBase2->Draw();
    pGBase3->Draw();

    // Versuche Grafiken auszurichten
    try
    {
        pGBase1->Align(*pGBase2); // 2 Bar-Objekte ausrichten; zulässig
        pGBase2->Align(*pGBase3); // Bar und Circle ausrichten; unzulässig
    }
    // Ausnahme vom Ausrichten auffangen
    catch (const char *const pT)
    {
        cout << "*** Exception: " << pT << "***\n";
    }

    // Grafiken nochmals ausgeben
    pGBase1->Draw();
    pGBase2->Draw();
    pGBase3->Draw();

    // Grafiken löschen
    delete pGBase1;
    delete pGBase2;
    delete pGBase3;

}

Übung:

Erstellen Sie Klassen für drei Grafikobjekte vom Typ Kreis, Rechteck und Polygon (Vieleck). Alle drei Klassen sind von einer gemeinsamen Basisklasse für Grafiken abzuleiten. Die Klassen selbst sollen nur eine Memberfunktion Draw(...) besitzen, die einen entsprechenden Text ausgibt (siehe nachfolgende Ausgabe).

Schreiben Sie dann eine Funktion (keine Memberfunktion!), die zufällig eines der drei Grafikobjekte erstellt und den Zeiger auf das erstellte Objekt zurück gibt. Eine solche Funktion, die nur zur Erstellung eines Objekts dient, wird auch als Klassenfabrik (class factory) bezeichnet. Da die Funktion sowohl einen Kreis, ein Rechteck oder ein Polygon erzeugen kann, muss Sie einen bestimmten Returntyp besitzen. Als kleiner Hinweis dazu: das Ganze hat etwas mit abgeleiteten Klassen und Zeigern zu tun.

Definieren Sie dann in main() ein Feld zur Aufnahme von zehn Zeigern auf die erzeugten Grafikobjekte und legen Sie in diesem Feld zehn zufällig erzeugte Grafiken ab. Geben Sie die Grafikobjekte zur Kontrolle aus.

Bestimmen Sie anschließend, wie oft jedes der drei Grafikelemente erzeugt wurde und geben das Ergebnis dann aus.

Polygon zeichnen
Kreis zeichnen
Rechteck zeichnen
Polygon zeichnen
Polygon zeichnen
Rechteck zeichnen
Kreis zeichnen
Polygon zeichnen
Rechteck zeichnen
Polygon zeichnen
2 Objekte von Typ class Circle
3 Objekte von Typ class Rect
5 Objekte von Typ class Polygon

Lösung ansehen!