C++ Core Guidelines: Regeln für Unions
Eine Union ist ein spezieller Datentyp, bei dem alle Mitglieder an derselben Adresse stehen. Sie kann zu einem Zeitpunkt immer nur ein Mitglied besitzen. Das spart Speicher. Eine "tagged"-Union ist eine Union, die über ihre Datentypen Buch führt.
- Rainer Grimm
Eine Union ist ein spezieller Datentyp, bei dem alle Mitglieder an derselben Adresse stehen. Sie kann zu einem Zeitpunkt immer nur ein Mitglied besitzen. Das spart Speicher. Eine "tagged"-Union ist eine Union, die über ihre Datentypen Buch führt.
Die C++ Core Guidelines bieten vier Regeln für Unions an. Hier sind sie.
- C.180: Use unions to save memory
- C.181: Avoid “naked” unions
- C.182: Use anonymous unions to implement tagged unions
- C.183: Don’t use a union for type punning
Los geht es mit der naheliegendsten Regel.
C.180: Use unions to save memory
Da eine Union einen Datentyp zu einem Zeitpunkt besitzen kann, spart sie Speicher. Die Union ist in diesem Fall so groß wie ihr größter innerer Datentyp.
union Value {
int i;
double d;
};
Value v = { 123 }; // now v holds an int
cout << v.i << '\n'; // write 123
v.d = 987.654; // now v holds a double
cout << v.d << '\n'; // write 987.654
Value ist eine "nackte" Union. Wer dem Wortlaut der nächsten Regel folgt, sollte sie nicht verwenden.
"Nackte" Unions sind sehr fehleranfällig, da du auf den zugrundeliegenden Datentyp selbst Buch führen musst.
// nakedUnion.cpp
#include <iostream>
union Value {
int i;
double d;
};
int main(){
std::cout << std::endl;
Value v;
v.d = 987.654; // v holds a double
std::cout << "v.d: " << v.d << std::endl;
std::cout << "v.i: " << v.i << std::endl; // (1)
std::cout << std::endl;
v.i = 123; // v holds an int
std::cout << "v.i: " << v.i << std::endl;
std::cout << "v.d: " << v.d << std::endl; // (2)
std::cout << std::endl;
}
Die Union besitzt in der ersten Iteration einen double- und in der zweiten Iteration einen int-Wert. Falls du ein double als ein int (1) interpretierst oder ein int als ein double (2), erhältst du undefiniertes Verhalten.
Um diese Fehlerquelle zu vermeiden, sollten "tagged"-Unions eingesetzt werden.
C.182: Use anonymous unions to implement tagged unions
Die Implementierung eines "tagged"-Union ist ziemlich aufwendig. Falls du neugierig bist, werfe ein Blick auf die Regel C.182. Ich werde es mir einfach machen und mich auf den neuen C++-Standard zurückziehen.
Mit C++17 erhalten wir "tagged"-Unions: std::variant. std::variant ist eine typ-sichere Union. Hier ist der erste Eindruck.
// variant.cpp
#include <variant>
#include <string>
int main(){
std::variant<int, float> v, w; // (1)
v = 12; // v contains int
int i = std::get<int>(v); // (2)
w = std::get<int>(v); // (3)
w = std::get<0>(v); // same effect as the previous line
w = v; // same effect as the previous line
// (4)
// std::get<double>(v); // error: no double in [int, float]
// std::get<3>(v); // error: valid index values are 0 and 1
try{
std::get<float>(w); // w contains int, not float: will throw
}
catch (std::bad_variant_access&) {}
// (5)
std::variant<std::string> v("abc"); // converting constructors work when unambiguous
v = "def"; // converting assignment also works when unambiguous
}
In (1) definierte ich die zwei Varianten v und w. Beide können einen int- und float-Wert besitzen. Ihr Startwert ist 0. Dies ist der Default-Welt ihres ersten zugrunde liegenden Datentyps. v wird zu 12. std::get<int>(v) gibt den Wert mithilfe des Datentyps zurück. Der Ausdruck (3) und die zwei folgenden Zeilen zeigen drei Möglichkeiten, die Variante w der Variante v zuzuweisen. Du musst dazu aber ein paar Regeln im Kopf behalten. Mithilfe des Datentyps oder des Index lässt sich eine Variante abfragen. Dabei muss der Datentyp eindeutig oder der Index gültig sein (4). Falls nicht, erhältst du eine std::bad_variant_access-Ausnahme. Falls der Aufruf des Konstruktors oder der des Zuweisungsoperators eindeutig ist, findet eine Konvertierung statt. Dies ist der Grund dafür, dass die Variante std::variant<std::string> mit eine C-String initialisiert oder ihr ein C-String zugewiesen (5) werden kann.
C.183: Don’t use a union for type punning
Zuerst einmal, was ist "type punning"? Es ist die Fähigkeit einer Programmiersprache, ihr Typsystem absichtlich zu untergraben, um einen Datentyp als einen anderen Datentyp zu behandeln. Eine typische Art "type punning" in C++ anzuwenden, ist es, eine Union nicht mit dem Datentyp zu lesen, mit der sie geschrieben wurde.
union Pun {
int x;
unsigned char c[sizeof(int)];
};
void bad(Pun& u)
{
u.x = 'x';
cout << u.c[0] << '\n'; // undefined behavior (1)
}
void if_you_must_pun(int& x)
{
auto p = reinterpret_cast<unsigned char*>(&x); // (2)
cout << p[0] << '\n'; // OK; better
// ...
}
Ausdrucke (1) besitzt zwei Probleme. Zuallererst besitzt er undefiniertes Verhalten. Darüber hinaus ist das "type punning" sehr schwer zu finden. Damit meine ich: Wenn du "type punning" einsetzen musst, verwende dazu einen explizite Konvertierung wie reinterpret_cast (2). Mit reinterpret_cast besitzt du zumindestens die Möglichkeit, im Nachhinein den Einsatz von "type punning" im Sourcecode zu lokalisieren.
Wie geht's weiter?
Zugegeben, dieser letzte Artikel zu Regeln für Klassen und Klassenhierarchien war ein wenig kurz. Mit dem nächsten Artikel geht es mit der nächsten größeren Kapitel weiter in den C++ Core Guidelines weiter: Aufzählungen.
Weitere Informationen:
- In dem Artikel "C++ besitzt einen Visitor" gehe ich weiter auf den neuen Datentyp std::variant ein.
- Für meine drei offenen Seminare im ersten Halbjahr 2018 sind noch Plätze frei. Ich freue mich immer darauf, meine Leidenschaft vermitteln zu können.
- Embedded-Programmierung mit modernem C++: 16. bis 18. Januar 2018
- C++11 und C++14: 13. bis 15. März 2018
- Multithreading mit modernem C++: 8. bis 9. Mai. 2018