C++ Core Guidelines: Mehr Regeln für Expressions

Modernes C++  –  18 Kommentare

Die Überschrift dieses Artikels ist vielleicht ein wenig langweilig: mehr Regeln für Expressions. Ehrlich gesagt, dieser Artikel beschäftigt sich vor allem mit Code-Hygiene, denn es geht um Zeiger.

Hier ist mein Plan:

Los geht's mit einer sehr wichtigen Regel:

ES.42: Keep use of pointers simple and straightforward

Die Guidelines bringen es deutlich auf den Punkt: "Complicated pointer manipulation is a major source of errors." Warum sollte uns das interessieren? Klar, unser Legacy-Code ist voll mit Altlasten wie im folgenden Beispiel:

void f(int* p, int count)
{
if (count < 2) return;

int* q = p + 1; // BAD

int n = *p++; // BAD

if (count < 6) return;

p[4] = 1; // BAD

p[count - 1] = 2; // BAD

use(&p[0], 3); // BAD
}

int myArray[100]; // (1)

f(myArray, 100), // (2)

Das dominante Problem dieses Codebeispiels ist es, dass der Aufruf der Funktion die richtige Länge des C-Arrays übergeben muss. Falls nicht, resultiert undefiniertes Verhalten.

Denke über die letzten zwei Zeilen (1) und (2) ein paar Sekunden nach. Die beginnen mit einem Array, von dem seine Typinformation entfernt wird, indem es als Argument der Funktion f verwendet wird. Dieser Prozess nennt sich "array to poiner decay" und ist der Grund für viele Fehler. Vielleicht hatte der Autor dieser Zeilen einen schlechten Tag und er zählte die Anzahl der Elemente falsch oder die Größe des C-Arrays ändert sich nachträglich. Egal, das Ergebnis ist immer dasselbe: undefiniertes Verhalten. Dieselbe Argumentation gilt natürlich auch für einen C-String.

Was sollen wir tun? Wir sollten den richtigen Datentyp verwenden. Die Guidelines empfehlen den Container gsl:span aus der Guidelines Support Library (GSL). Hier ist das verbesserte Programmschnipsel:

void f(span<int> a) // BETTER: use span in the function declaration
{
if (a.length() < 2) return;

int n = a[0]; // OK

span<int> q = a.subspan(1); // OK

if (a.length() < 6) return;

a[4] = 1; // OK

a[count - 1] = 2; // OK

use(a.data(), 3); // OK
}

Gut! gsl::span prüft zur Laufzeit seine Grenzen. Zusätzlich besitzt die Guidelines Support Library eine freie Funktion at, mit der sich die Elemente von gls::span direkt ansprechen lassen.

void f3(array<int, 10> a, int pos) 
{
at(a, pos / 2) = 1; // OK
at(a, pos - 1) = 2; // OK
}

Warum sollte ein std::array statt eines C-Arrays, ein gsl::stack_array statt eines C-Arrays eingesetzt werden?

Ich höre bereits Bedenken. Die meisten von uns setzen die Guidelines Support Library nicht ein. Die Funktion f und f3 lassen sich direkt mithilfe des Containers std::array und seiner Method std::array::at neu formulieren. Hier sind sie:

// spanVersusArray.cpp

#include <algorithm>
#include <array>

void use(int*, int){}

void f(std::array<int, 100>& a){

if (a.size() < 2) return;

int n = a.at(0);

std::array<int, 99> q;
std::copy(a.begin() + 1, a.end(), q.begin()); // (1)

if (a.size() < 6) return;

a.at(4) = 1;

a.at(a.size() - 1) = 2;

use(a.data(), 3);
}

void f3(std::array<int, 10> a, int pos){
a.at(pos / 2) = 1;
a.at(pos - 1) = 2;
}

int main(){

std::array<int, 100> arr{};

f(arr);

std::array<int, 10> arr2{};

f3(arr2, 6);

}

Der std::array::at-Operator prüft zur Laufzeit seine Grenzen. Falls pos >= size, wirft er eine std::out_of_range-Ausnahme. Wer Programm spanVersusArray.cpp sorgfältig studiert, dem fallen zwei Einschränkungen auf. Zuerst ist der Ausdruck (1) deutlich verboser als die entsprechende gls::span-Version. Darüber hinaus ist die Länge des std::array Bestandteil der Signatur der Funktion. Das ist sehr schlecht. In diesem Fall kann die Funktion nur mit dem Container std::array<int, 100> verwendet werden. Damit ist auch die Prüfung der Array-Größe im Funktionskörper überflüssig.

Zu unserer Rettung besitzt C++ Templates. Daher ist einfach, diese Einschränkungen zu überwinden und trotzdem typsicher zu bleiben.

// at.cpp

#include <algorithm>
#include <array>
#include <deque>
#include <string>
#include <vector>

template <typename T>
void use(T*, int){}

template <typename T>
void f(T& a){

if (a.size() < 2) return;

int n = a.at(0);

std::array<typename T::value_type , 99> q; // (4)
std::copy(a.begin() + 1, a.end(), q.begin());

if (a.size() < 6) return;

a.at(4) = 1;

a.at(a.size() - 1) = 2;

use(a.data(), 3); // (5)
}

int main(){

std::array<int, 100> arr{};
f(arr); // (1)

std::array<double, 20> arr2{};
f(arr2); // (2)

std::vector<double> vec{1, 2, 3, 4, 5, 6, 7, 8, 9};
f(vec); // (3)

std::string myString= "123456789";
f(myString); // (4)

// std::deque<int> deq{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// f(deq); // (5)

}

Jetzt lässt sich die Funktion f auf std::arrays's beliebiger Länge (1) und beliebigen zugrunde liegenden Datentyps (2) anwenden. Darüber hinaus kann sie mit einem std::vector (3) oder einem std::string (4) aufgerufen werden. Diesen Containern ist gemeinsam, dass ihre Daten einem kontinuierlichen Speicherbereich gespeichert werden. Das gilt nicht für std::deque, und somit schlägt der Aufruf a.data() im Ausdruck (5) fehl. Ein std::deque ist eine Art doppelt verkettete Liste von kleinen Speicherbereichen.

Der Ausdruck T::value_type (5) erlaubt es, den zugrunde liegenden Typ eines Containers zu erhalten. T ist ein sogenannter Dependent Type, den T ist ein Typ-Parameter des Funktions-Templates f. Das ist der Grund, dass ich dem Compiler unter die Arme greifen und ihm einen Hinweis geben muss, dass er in diesem Fall T::value_type als Typ interpretieren soll: typename T::value_type.

ES.45: Avoid “magic constants”; use symbolic constants

Diese Regel sollte selbstverständlich sein: Eine symbolische Konstante sagt mehr aus als eine magische Konstante. Das Beispiel der Guidelines beginnt in diesem Fall mit einer magischen Konstanten, fährt mit einer symbolischen Konstanten fort und endet mit einer Range-basierten for-Anweisung.

for (int m = 1; m <= 12; ++m)        // don't: magic constant 12
cout << month[m] << '\n';



// months are indexed 1..12 (symbolic constant)
constexpr int first_month = 1;
constexpr int last_month = 12;

for (int m = first_month; m <= last_month; ++m) // better
cout << month[m] << '\n';



for (auto m : month) // the best (ranged-based for loop)
cout << m << '\n';

Im Falle dieser Anweisung ist ein sogenannter off-by-one-Fehler schlicht und ergreifend nicht möglich.

Jetzt springe ich direkt zur Regel ES.47, denn ich will die Regeln zur Konvertierung von Daten, die die Regel ES.47 mit einschließt, in einem separaten Artikel vorstellen.

ES.47: Use nullptr rather than 0 or NULL

Es gibt viele gute Gründe, einen nullptr anstelle der Zahl 0 oder dem Makro NULL zu verwenden. Insbesondere sind die Zahl 0 oder das Makro NULL für generischen Code ungeeignet. Ich habe bereits einen Artikel zu den drei Varianten eines Nullzeigers verfasst. Hier sind die Details: "Die Null-Zeiger-Konstante nullptr".

Wie viele explizite Casts gibt es in modernem C++? Du kommst vermutlich auf die Zahl 4. Diese Antwort ist falsch. In C++11 gibt es 6 verschiedene, explizite Casts. Wenn ich noch die GSL hinzurechnen, komme ich bereits auf 8 verschiedene, explizite Casts. Genau diese 8 explizite Casts sind das Thema des nächsten Artikels.