C++ Core Guidelines: Type Safety

Modernes C++  –  3 Kommentare

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ügt const und volatile hinzu
  • reinterpret_cast: Konvertieren zwischen Zeigern oder zwischen integralen Datentypen und Zeigern
  • dynamic_ cast: konvertiert zwischen polymorphen Zeigern oder Referenzen in derselben Klassenhierarchie
  • std::move: konvertiert in eine Rvalue-Referenz
  • std::forward: konvertiert einen Lvalue in eine Lvalue-Referenz und einen Rvalue in eine Rvalue-Referenz
  • gsl::narrow_cast: wendet ein static_cast an
  • gsl::narrow: wendet ein static_cast an

Okay, die Beschreibung ist zu kompakt. Daher möchte ich zumindest zwei Bemerkungen machen:

  1. 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.
  2. std::move und std::forward sind Casts? Lass mich einen genaueren Blick auf std::move werfen: static_cast<std::remove_reference<decltype(arg)>::type&&>(arg). Zuerst wird der Typ des Arguments arg mithilfe von decltype(arg) bestimmt. Danach werden alle Referenzen entfernt und zwei neue hinzugefügt. Die Funktion std::remove_reference ist aus der Type-Traits-Bibliothek. Ich habe bereits ein paar Artikel zur Type-Traits-Bibliothek geschrieben. Am Ende verlässt arg 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 away const

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.