C++ Core Guidelines: Type Safety
Die C++ Core Guidelines bieten drei Profile an: Type Safety, Bounds Safety und Lifetime Safety. Dank der Guideline Support Library (GSL) lässt sich der Sourcecode gegen die drei Profile testen. Dieser Artikel beschäftigt sich mit der Type Safety.
- Rainer Grimm
Die C++ Core Guidelines bieten drei Profile an: Type Safety, Bounds Safety und Lifetime Safety. Dank der Guideline Support Library (GSL) lässt sich der Sourcecode gegen die drei Profile testen. Dieser Artikel beschäftigt sich mit der Type Safety.
Falls du nicht weißt, für was ein Profil steht, lies meinen letzten Artikel: C++ Core Guidelines: Profile. Obwohl es die Idee eines Profils ist, ein spezifisches Ziel zu erreichen, benötigt ein Profil Unterstützung der anderen Profile. Dies heißt, dass das Profil Type Safety Unterstützung der Profile Bounds Safety und Lifetime Safety benötigt. Jetzt geht es aber los mit der Type Safety.
Type Safety
Type Safey bedeutet, dass du deine Datentypen richtig verwendest und daher nicht gezwungen bist, unsichere Cast und Unions anzuwenden. Type Safey besteht aus acht Regeln, die Type genannt werden. Die Regeln starten mit "don't", "always" oder "avoid" und beziehen sich auf bestehende Regeln der C++ Core Guidelines. Falls es notwendig ist, werde ich zusätzliche Information zu den Regeln hinzufügen.
Type 1:
- Don’t use reinterpret_cast
- Don’t use static_cast for arithmetic types
- Don’t cast between pointer types where the source type and the target type are the same
- Don’t cast between pointer types when the conversion could be implicit
Die Antwort auf die "don't" lassen sich auf zwei Punkte reduzieren. Vermeide Casts und ziehe, wenn notwendig, benamte C++-Casts vor.
- Vermeide Casts
Was passiert, wenn ich das Typsystem pervertiere?
// casts.cpp
#include <iostream>
int main(){
double d = 2;
auto p = (long*)&d;
auto q = (long long*)&d;
std::cout << d << ' ' << *p << ' ' << *q << '\n';
}
Weder das Ergebnis mit dem Visual Studio Compiler
noch das Ergebnis mit dem GCC- oder Clang-Compiler ist beruhigend:
Was ist das Problem mit dem C-Cast? Du kannst nicht erkennen, welcher Cast unter der Decke angewandt wurde. Falls du einen C-Cast verwendest, wird vereinfachend gesprochen, eine Kombination von Casts verwendet. Los geht es mit dem static_cast, gefolgt vom const_cast und zuletzt der reinterpret_cast.
Es gibt ein weites Problem mit C-Casts. Es ist ziemlich anspruchsvoll, C-Casts im Sourcecode zu finden. Dies gilt nicht fĂĽr C++-Casts wie den dynamic_cast, const_cast, static_cast oder reinterpret_cast.
NatĂĽrlich ahnst du bereits, wie es weitergeht: "Explicit is better than implict."
- Ziehe benamte C++-Casts vor
Rechne ich die GSL hinzu, bietet C++ acht verschiedene Casts an. Hier sind sie inklusive einer kurzen Beschreibung:
-
static_cast: konvertiert zwischen ähnlichen Datentypen wie Zeiger oder numerischen Typen const_cast: entfernt oder fügtconstundvolatilehinzureinterpret_cast: Konvertieren zwischen Zeigern oder zwischen integralen Datentypen und Zeigerndynamic_ cast: konvertiert zwischen polymorphen Zeigern oder Referenzen in derselben Klassenhierarchiestd::move: konvertiert in eine Rvalue-Referenzstd::forward: konvertiert einen Lvalue in eine Lvalue-Referenz und einen Rvalue in eine Rvalue-Referenzgsl::narrow_cast: wendet einstatic_castangsl::narrow: wendet einstatic_castan
Okay, die Beschreibung ist zu kompakt. Daher möchte ich zumindest zwei Bemerkungen machen:
- GSL steht für die Guideline Support Library. Dies ist eine Header-Only Library in dem Namensraum gsl. Die GSL lässt sich dazu verwenden, automatisch die Regeln der C++ Core Guidelines und insbesondere die Profile zu prüfen. Über diese Prüfungen werde ich in einem zukünftigen Artikel schreiben. Zur GSL habe ich bereits einen Artikel geschrieben: C++ Core Guidelines: The Guideline Support Library.
std::moveundstd::forwardsind Casts? Lass mich einen genaueren Blick aufstd::movewerfen:static_cast<std::remove_reference<decltype(arg)>::type&&>(arg). Zuerst wird der Typ des Argumentsargmithilfe vondecltype(arg)bestimmt. Danach werden alle Referenzen entfernt und zwei neue hinzugefügt. Die Funktionstd::remove_referenceist aus der Type-Traits-Bibliothek. Ich habe bereits ein paar Artikel zur Type-Traits-Bibliothek geschrieben. Am Ende verlässtarg std::moveimmer als Rvalue-Referenz.
Type 2:
- Don’t use
static_castto downcast
Gerne möchte ich eine kurze Antwort geben: Verwende dynamic_cast. Auch zu diesem Thema habe bereits einen Artikel verfasst: C++ Core Guidelines: Zugriffe auf Objekte in Klassenhierarchien
Type 3:
- Don’t use
const_castto cast awayconst
Jetzt muss ich ein wenig genauer argumentieren. const von einem Objekt wegzucasten, stellt undefiniertes Verhalten dar, falls das Objekt wie constInt nicht veränderlich war:
const int constInt = 10;
const int* pToConstInt = &constInt;
int* pToInt = const_cast<int*>(pToConstInt);
*pToInt = 12; // undefined behaviour
Falls du mir nicht glaubst, es gibt eine FuĂźnote im C-Standard [ISO/IEC 9899:2011] (subclause 6.7.3, paragraph 4), der auch Relevanz fĂĽr den C++-Standard besitzt: "The implementation may place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used." Das heiĂźt, dass eine Modifikation auf einem ursprĂĽnglichen konstanten Objekte keine Auswirkung haben kann.
Type 4:
- Don’t use C-style (T)expression or functional T(expression) casts
Der erste Teil dieses "don't" ist recht einfach zu beantworten: Verwende benamte Casts wie im Type 1.
Der funktionale T(e) Cast wird dazu verwendet, ein T aus einem Ausdruck e zu erzeugen. Was passiert, wenn der funktionale Cast falsch verwendet wird?
// functionalCast.cpp
void f(int x, long y, double d, long long l){
char c1(x);
char c2(y);
char c3(d);
char c4(l);
}
int main(){
f(3, 3l, 3.0, 3ll);
}
Die Funktion f nimmt vier Argumente an und verwendet diese Argumente um chars zu initialisieren. Du erhältst in diesem Fall, was du verdienst, und kannst nur auf Warnungen des Compilers hoffen. C++ Insights zeigt explizit, wie dein Code transformiert wird. Ein static_cast wird auf jedes Argument angewandt.
Dieser Prozess wird Narrowing Conversion genannt und sollte durch den Compiler entdeckt werden. Dank der Verwendung von geschweiften Klammern prĂĽft der Compiler, ob Narrowing Conversion vorliegt. Der Compiler muss in diesem Fall eine Warnung schreiben, interpretiert diese Warnung aber typischerweise als Fehler. Falls du auf Nummer sicher gehen willst, dass Narrowing Conversion immer einen Fehler erzeugt, kannst du mit dem GCC und Clang -Werror=narrowing verwenden. Hier ist das leicht modifizierte Programm:
// functionalCastCurlyBraces.cpp
void f(int x, long y, double d, long long l){
char c1{x};
char c2{y};
char c3{d};
char c4{l};
}
int main(){
f(3, 3l, 3.0, 3ll);
}
Der Compiler entdeckt, was schiefläuft.
Wie geht's weiter?
Mit meinem nächsten Artikel werde ich die Regeln zu Type Safety vollenden. Bei ihnen geht es um die Initialisierung, Unions und Varargs. Nun muss ich mich aber für eine sehr aufregende Woche vorbereiten. Ich werde einen Zwei-Tages-Workshop zur Concurrency, einen Back-to-Basics-Vortrag und einen Vortrag zu Concepts auf der CppCon halten. ()