Varianten
Mithilfe einer Variante, auch als Union bezeichnet, lassen sich auf ein und demselben Speicherplatz unterschiedliche Daten ablegen bzw. dort abgelegte Daten unterschiedlich interpretieren.
Definition
Die Definition einer Variante gleicht bis auf das Schlüsselwort union der bisherigen Definition einer Klasse. Genau genommen ist eine Variante ein spezieller Klassentyp, der eine Reihe von Einschränkungen besitzt.
union [NAME]
{
DTYP1 data1;
DTYP2 data2;
... // weitere Eigenschaften und Memberfkt.
} [varObj1,...];
Die in Klammern stehenden Angaben sind wiederum optional.
Alle Eigenschaften einer Variante liegen ab derselben Adresse im Speicher. Demzufolge wird der für die Variante benötigte Speicherplatz von der Eigenschaft mit dem größten Speicherbedarf bestimmt.
Varianten können alle bisher bekannten Datentypen enthalten mit folgenden Einschränkungen: Ein Variante darf
- keine virtuellen Methoden enthalten,
- keine Referenzen enthalten und
- kann keine Basisklasse sein
Die meisten der Einschränkungen dürften im Augenblick noch nicht viel sagen, da die dort aufgeführte Funktionalität erst später beschrieben wird. Diese Einschränkungen wurden hier aber trotzdem aufgeführt, um das Thema Varianten so weit wie möglich zusammenzuhalten.
Der Zugriff auf ein Variantenmember erfolgt analog zum Zugriff auf ein Klassenmember, d.h., es folgt zuerst der Name des Variantenobjekts, dann bei direktem Zugriff der Punktoperator bzw. bei indirektem Zugriff der Zeigeroperator und zum Schluss der Name des Variantenmembers.
#include <print>
// Variante
union Convert
{
private:
unsigned char bytes[2];
unsigned short word;
public:
// Methoden zum Setzen der Bytes
void SetByte1(unsigned char byte)
{
bytes[0] = byte;
}
void SetByte2(unsigned char byte)
{
bytes[1] = byte;
}
// Bytes als Word zurückgeben
auto GetWord() const
{
return word;
}
};
int main()
{
const unsigned char val1=0x11, val2=0x22;
std::print("Byte {:#x} und {:#x} als Wort: ",
val1,val2);
// Varianten-Objekt definieren
Convert byte2Word;
// Bytes setzen
byte2Word.SetByte1(val1);
byte2Word.SetByte2(val2);
// Als unsigned short ausgeben
std::println("{:#x}\n",byte2Word.GetWord());
}
Byte 0x11 und 0x22 als Wort: 0x2211
Im Beispiel wird eine Variante Convert definiert, die zwei unsigned char-Werte zu einem unsigned short-Wert zusammenfasst. Zum Setzen der char-Werte werden die Methoden SetByteX() verwendet. Der aus den beiden char-Werten zusammengesetzte short-Wert wird dann über die Methode GetWord() ausgelesen.
Anonyme Variante
Anonyme Varianten besitzen keinen Variantennamen und es können von diesem Typ keine Variantenobjekte definiert werden.
Anonyme Varianten weisen zwei Besonderheiten auf:
- Sie müssen immer der Speicherklasse static angehören wenn sie im globalen oder benannten Namensraum definiert werden. Werden sie lokal definiert, können sie jeder Speicherklasse angehören, die an der entsprechenden Stelle erlaubt ist.
- Sie können keine Methoden enthalten.
Anonyme Varianten dienen 'nur' zur Ablage von verschiedenen Daten ab der gleichen Speicherstelle. Alle Member der anonymen Variante werden direkt angesprochen, d.h. ohne die sonst übliche Varianten-Syntax x.y bzw. x->y. Das folgende Beispiel demonstriert den Einsatz einer anonymen Variante zur Konvertierung eines unsigned long-Werts in zwei unsigned short bzw. vier unsigned char-Werte.
#include <iostream>
#include <print>
// Definition der anonymen Variante
static union
{
unsigned long lVal;
unsigned short sVal[2];
unsigned char cVal[4];
};
int main()
{
// ulong-Anteil der anonymen Variante
lVal = 0x12345678UL;
// Ausgabe als ulong
std::println("long-Wert: {:#x}", lVal);
// Ausgabe als ushort
std::println("Als short: {:#x} {:#x}",sVal[0], sVal[1]);
// Ausgabe als uchar
std::print("Als char : ");
for (auto elem: cVal)
std::print("{:#x} ",elem);
std::cout << '\n';
}
long-Wert: 0x12345678
Als short: 0x5678 0x1234
Als char : 0x78 0x56 0x34 0x12
Initialisierung einer Variante
Fehlt zum Abschluss noch eine Abweichung der Variante von einer struct bzw. class Klasse. Sie betrifft die Initialisierung der Variante. Soll ein Variantenobjekt bei seiner Definition initialisiert werden, muss bis auf Weiteres der Datentyp des Initialwertes mit dem Datentyp des 'ersten' Elements in der Variante übereinstimmen. Außerdem ist der Initialisierungswert in einen Block {...} einzuschließen.
// Variante definieren
union Month
{
char cMonth;
char *ptrMonth; };
// Ungültige Initialisierungen da Datentyp des
// Initialwertes nicht übereinstimmt
union Month month1 {"Januar"};
// Gültige Initialsierung
union Month month2 {10};
Später werden wir ein Verfahren kennenlernen, das es erlaubt, jedes beliebige Element einer Variante zu initialisieren (Stichwort: Überladen des Konstruktors).
Für den typsicheren Zugriff auf die Eigenschaften einer Variante enthält die Standardbibliothek das Template std::variant. Was Templates sind und welchen Vorteil std::variant bietet, sehen wir uns später an.
Übungen
variant_01:
Zum Abspeichern einer Farbe ist eine Variante CConvert zu entwickeln. Die Farbe soll sowohl als unsigned long-Wert wie auch als drei unsigned char RGB-Werte abgelegt und ausgelesen werden können. Die Farbeigenschaft ist als private-Eigenschaft zu definieren.
Zum Setzen und Auslesen der Farbe sind Methoden zu erstellen. Die Farbe soll sowohl als unsigned long-Wert wie auch als RGB-Werte übergeben werden können. D.h., es werden vier Methoden benötigt.
Konvertieren Sie als Erstes den Farbwert 0x112233 in RGB-Werte und geben diese aus, wobei für Rot 0x11, Grün 0x22 und Blau 0x33 einzustellen ist.
Konvertieren Sie danach die RGB Werte Rot 0xaa, Grün 0xbb und Blau 0xcc in einen unsigned long Farbwert und geben diesen ebenfalls aus.
Definieren Sie die Variante in einem Modul!
Rot: 0x11, Gruen: 0x22, Blau: 0x33
Farbe: 0xaabbcc