C++ Core Guidelines: Regeln für Konvertierungen und Casts

Was haben Konvertierungen und Casts gemein? Sie sind sehr häufig die Ursache von Fehlern. Daher geht es nun um typische Fehler.

In Pocket speichern vorlesen Druckansicht 25 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Rainer Grimm

Was haben Konvertierungen und Casts gemein? Sie sind sehr häufig die Ursache von Fehlern. Daher geht es nun um typische Fehler.

Hier sind die Regeln der Guidelines:

Verengende Konvertierung (narrowing conversion) ist eine Konvertierung, die mit einem Verlust der Datengenauigkeit einhergeht. Meistens ist das nicht im Sinne des Autors.

ES.46: Avoid narrowing conversions

Hier sind ein paar Beispiele aus den Guidelines:

double d = 7.9;
int i = d; // bad: narrowing: i becomes 7
i = (int) d; // bad: we're going to claim this is still not explicit enough

void f(int x, long y, double d)
{
char c1 = x; // bad: narrowing
char c2 = y; // bad: narrowing
char c3 = d; // bad: narrowing
}

Falls du verengende Konvertierung anwenden willst, solltest du es explizit und nicht implizit tun. Dies ist gemäß der Python-Regel aus "The Zen of Python": "Explit is betten than implict." Die Guideline Support Library (GSL) besitzt zwei Casts, um die Intention auf den Punkt zu bringen: gsl::narrow_cast und gsl::narrow.

double d = 7.9;
i = narrow_cast<int>(d); // OK (you asked for it): narrowing: i becomes 7
i = narrow<int>(d); // OK: throws narrowing_error

Der gsl::narrow_cast führt den Cast aus und der gsl::narrow Cast wirft eine Ausnahme, falls eine verengende Konvertierung stattfindet.

Meistens passiert die verengende Konvertierung heimlich. Wie kannst du dich davor schützen? Wende geschweifte Klammern an:

void f(int x, long y, double d){
char c1 = {x};
char c2 = {y};
char c3 = {d};
}

int main(){

double d = {7.9};
int i = {d};

f(3, 3l, 3.0);
}

Alle Initialisierungen habe ich in geschweifte Klammern verpackt. Entsprechend dem C++11-Standard, muss mich der Compiler warnen, wenn eine verengende Konvertierung stattfindet.

"Explicit is better than implicit." Dieser Grundsatz gilt nicht für C-Casts.

ES.48: Avoid 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 schlecht am C-Cast? Du kannst nicht erkennen, welcher Cast angewandt wurde. Falls du einen C-Cast verwendest, wird vereinfachend gesprochen, eine Kombination von Casts gegebenenfalls angewendet. Los geht es mit dem static_cast, gefolgt vom const_cast und zuletzt der reinterpret_cast.

Natürlich ahnst du bereits, wie es weitergeht: "Explicit is better than implict."

ES.49: If you must use a cast, use a named cast

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 in eine Rvalue-Referenz
  • gsl::narrow_cast: wendet ein static_cast an
  • gsl::narrow: wendet ein static_cast an

std::move und std::forward sind Casts? Lasse mich einen genaueren Blick darauf werfen, was std::move unter der Decke tut:

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 erhalten wir immer eine Rvalue-Referenz.

Die Eigenschaft const von einem Objekt wegzucasten, stellt undefiniertes Verhalten dar.

ES.50: Don’t 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 ein Fußnote in C-Standard [ISO/IEC 9899:2011] (subclause 6.7.3, paragraph 4), der 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."

Habe ich nicht veränderlich geschrieben? Veränderlich oder mutable ist eines der unbekanntesten Features in C++. mutable erlaubt es, zwischen der bitweisen und logischen Konstanz eines Objekts zu unterscheiden. Was?

Stelle dir vor, du sollst eine Schnittstelle zu einem Telefonbuch implementieren. Der Einfachheit halber landen dabei die Einträge in einem std::unordered_map.

// teleBook.cpp

#include <iostream>
#include <string>
#include <unordered_map>

std::unordered_map<std::string, int> getUpdatedTelephoneBook(){
// generate a new, updated telephone book
return {{"grimm",123}, {"huber", 456}, {"schmidt", 321}};
}

class TelephoneBook{
public:
int getNumber(const std::string& name) const {
auto ent = cache.find(name);
if(ent != cache.end()){
return ent->second;
}
else{
cache = getUpdatedTelephoneBook(); // (2)
return cache[name];
}
}
private: // (1)
std::unordered_map<std::string, int> cache = {{"grimm",123},
{"huber", 456}};
};


int main(){

std::cout << std::endl;

TelephoneBook telBook; // (3)

std::cout << "grimm " << telBook.getNumber("grimm") << std::endl;

std::cout << "schmidt " << telBook.getNumber("schmidt") << std::endl;

std::cout << std::endl;

}

Das Telefonbuch (1) ist sehr klein. Normalerweise ist es deutlich größer, und es zu aktualisieren ist eine teure Operation (2). Das bedeutet, die Aktualisierung eines Telefonbuchs findet nur einmal jährlich statt. Vom konzeptionellen Blickwinkel betrachtet, sollte das teleBook (3) konstant sein. Dies ist nicht möglich, da die std::unorderd_map in der Funktion getNumber verändert wird. Hier ist der Beweis in roten Ellipsen.

Der Qualifizierer mutable erlaubt es, zwischen der bitweisen und der logischen Konstanz zu unterscheiden. Das Telefonbuch ist logisch, aber nicht bitweise konstant.

// teleBook.cpp

#include <iostream>
#include <string>
#include <unordered_map>

std::unordered_map<std::string, int> getUpdatedTelephoneBook(){
// generate a new, updated telephone book
return {{"grimm",123}, {"huber", 456}, {"schmidt", 321}};
}

class TelephoneBook{
public:
int getNumber(const std::string& name) const {
auto ent = cache.find(name);
if(ent != cache.end()){
return ent->second;
}
else{
cache = getUpdatedTelephoneBook(); // (2)
return cache[name];
}
}
private: // (1)
mutable std::unordered_map<std::string, int> cache = {{"grimm",123},
{"huber", 456}};
};


int main(){

std::cout << std::endl;

const TelephoneBook telBook; // (3)

std::cout << "grimm " << telBook.getNumber("grimm") << std::endl;

std::cout << "schmidt " << telBook.getNumber("schmidt") << std::endl;

std::cout << std::endl;

}

Ich füge lediglich const (3) zu telBook hinzu und mutable zu cache (1). Damit verhält sich das Programm wie erwartet.

ES.55: Avoid the need for range checking

Jetzt kann ich mich kurz halten. Falls die Range-basierte for-Anweisung oder Algorithmen der STL zum Einsatz kommen, gibt es keine Notwendigkeit, die Bereichsgrenzen zu prüfen.

std::array<int, 10> arr = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3}; 
std::sort(arr.begin(), arr.end());
for (auto a : arr) {
std::cout << a << " ";
}
// 0 1 2 3 4 5 6 7 8 9

In meinem nächsten Artikel zu Expressions werde ich über std::move, new und delete und slicing schreiben. Slicing ist wohl eines der dunkelsten Ecken von C++. ()