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ügtconst
undvolatile
hinzureinterpret_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_cast
angsl::narrow
: wendet einstatic_cast
an
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::move
undstd::forward
sind Casts? Lass mich einen genaueren Blick aufstd::move
werfen:static_cast<std::remove_reference<decltype(arg)>::type&&>(arg)
. Zuerst wird der Typ des Argumentsarg
mithilfe vondecltype(arg)
bestimmt. Danach werden alle Referenzen entfernt und zwei neue hinzugefügt. Die Funktionstd::remove_reference
ist aus der Type-Traits-Bibliothek. Ich habe bereits ein paar Artikel zur Type-Traits-Bibliothek geschrieben. Am Ende verlässtarg std::move
immer als Rvalue-Referenz.
Type 2:
- Don’t use
static_cast
to 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_cast
to 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 char
s
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. ()