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

Modernes C++  –  11 Kommentare

In diesem Artikel werde ich die Regeln für Deklarationen abschließen. Die verbleibenden Regeln sind nicht besonders anspruchsvoll aber sehr wichtig für guten Code.

Und schon geht es weiter. Hier ist der erste Überblick zu den Regeln, bevor wir in die Details eintauchen.

In Python gibt es einen beliebten Aphorismus des Zen von Python (Tim Peters): "Explicit is betten than implicit." Dieser stellt eine Meta-Regel für das Schreibe von gutem Code in Python dar. Genau diese Meta-Regel trifft auch auf die ersten zwei vorgestellten Regeln der C++ Core Guidelines zu.

ES.25: Declare an object const or constexpr unless you want to modify its value later on

Warum sollten const oder constexpr für Variablen-Deklarationen verwendet werden? Ich kenne viele gute Gründe:

  • Du bringst deine Absicht genau auf den Punkt.
  • Deine Variable kann nicht zufällig verändert werden.
  • const- oder constexpr-Variablen sind per Definition thread-sicher.
    • const: Du musst lediglich sicherstellen, dass die Variable thread-sicher initialisiert wird.
    • constexpr: Die C++-Laufzeit sichert zu, dass die Variable thread-sicher initialisiert wird.

ES.26: Don’t use a variable for two unrelated purposes

Ist dies guter Code?

void use()
{
int i;
for (i = 0; i < 20; ++i) { /* ... */ }
for (i = 0; i < 200; ++i) { /* ... */ } // bad: i recycled
}

Ich denke nicht. Verschiebe die Deklaration von i in die for-Schleife. Damit wird die Gültigkeit von i an die Gültigkeit der for-Schleife gebunden.

void use()
{
for (int i = 0; i < 20; ++i) { /* ... */ }
for (int i = 0; i < 200; ++i) { /* ... */ }
}

Mit C++17 lässt sich eine Variable direkt in einer switch- oder einer if-Anweisung deklarieren: "C++17: Was gibt's Neues in der Kernsprache?"

ES.27: Use std::array or stack_array for arrays on the stack

Vor gut Zehn Jahren ist es mir selbst passiert. Ich dachte, dass das Anlegen eines Arrays variabler Länge ISO C++ darstellt.

const int n = 7;
int m = 9;

void f()
{
int a1[n];
int a2[m]; // error: not ISO C++
// ...
}

Falsch!

Im ersten Fall solltest du ein std::array anwenden und im zweiten Fall ein gsl::stack_array aus der Guideline Support Library (GSL).

const int n = 7;
int m = 9;

void f()
{
std::array<int, n> b1;
gsl::stack_array<int> b2(m);
// ...
}

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

std::array kennt im Gegensatz zum C-Array seine Länge und wird nicht zum Zeiger auf sein erstes Element degradiert, wenn es als Parameter einer Funktion verwendet wird. Wie leicht ist es, die folgenden Funktion zum Kopieren von C-Arrays mit der falschen Länge n aufzurufen:

void copy_n(const T* p, T* q, int n); // copy from [p:p+n) to [q:q+n)

Arrays variabler Länge wie a2[m] stellen ein Sicherheitsrisiko dar, da sie es erlauben können, beliebigen Code auszuführen oder den Stack zu konsumieren.

ES.28: Use lambdas for complex initialization, especially of const variables

Immer wieder kommt die Frage in meinen Seminaren auf: Warum soll ich eine Lambda-Funktion direkt an Ort und Stelle aufrufen? Diese Regel gibt eine Antwort. In ihr lässt sich die aufwendige Initialisierung schön verpacken. Dieser Aufruf an Ort und Stelle ist sehr wichtig, wenn der Wert const werden soll.

Falls du einen Wert nach seiner Initialisierung nicht mehr verändern willst, solltest du ihn entsprechend der vorherigen Regel R.25 const deklarieren. Gut! Aber manchmal wird eine Variable mehrstufig initialisiert. Daher kann sie nicht als const deklariert werden.

Das widget x in dem folgenden Beispiel sollte nach seiner Initialisierung konstant sein. Das lässt die C++-Syntax aber nicht zu, da x während seiner Initialisierung mehrfach verändert wird.

widget x;   // should be const, but:
for (auto i = 2; i <= N; ++i) { // this could be some
x += some_obj.do_something_with(i); // arbitrarily long code
} // needed to initialize x
// from here, x should be const, but we can't say so in code in this style

Die Rettung naht in der Gestalt einer Lambda-Funktion. Schiebe die Initialisierung der Variable in eine Lambda-Funktion, binde die Umgebung per Referenz und initialisiere deine konstante Variable mit einer an Ort und Stelle aufgerufenen Lambda-Funktion.

const widget x = [&]{
widget val; // widget has a default constructor
for (auto i = 2; i <= N; ++i) { // this could be some
val += some_obj.do_something_with(i); // arbitrarily long code
} // needed to initialize x
return val;
}();

Zugegeben, es sieht schon ein wenig befremdlich aus, eine Lambda-Funktion an Ort und Stelle aufzurufen. Vom konzeptionellen Blickwinkel betrachtet, ist es sehr elegant. Der ganze Initialisierungsaufwand ist schön in einem Funktionskörper verpackt.

ES.30, ES.31, ES.32 und ES.33

Diese vier Regeln werde ich nur kurz zusammenfassen. Verwende keine Makros für Textmanipulationen oder für Konstanten und Funktionen. Falls du Makros verwenden musst, gib ihnen eindeutige Namen in GROSS_BUCHSTABEN.

ES.34: Don’t define a (C-style) variadic function

Genau! Verwende keine C-Style-Variadic-Funktionen! Seit C++11 besitzen wir Variadic Templates und mit C++17 Fold Expressions. Das ist alles, was wir benötigen.

Du hast sicherlich bereits sehr oft die C-Style-Variadi-Funktion printf verwendet. printf nimmt einen Fomat-String und eine beliebige Anzahl an Argument an und stellt diese dar. Ein Aufruf von printf besitzt undefiniertes Verhalten, falls du die falschen Formatangaben verwendest oder die Anzahl der Argumente nicht passt.

Dank Variadic Templates lässt sich eine typsichere printf-Funktion implementieren. Hier ist eine vereinfachte Variante von printf, basierend auf cppreference.com.

// myPrintf.cpp

#include <iostream>

void myPrintf(const char* format){ // (1)
std::cout << format;
}

template<typename T, typename... Targs> // (2)
void myPrintf(const char* format, T value, Targs... Fargs)
{
for ( ; *format != '\0'; format++ ) {
if ( *format == '%' ) {
std::cout << value; // (3)
myPrintf(format+1, Fargs...); // (4)
return;
}
std::cout << *format;
}
}

int main(){
myPrintf("% world% %\n","Hello",'!',123); // Hello world! 123
}

myPrintf kann eine beliebige Anzahl an Elementen annehmen. Falls beliebig 0 bedeutet, wird die erste überladene Variante (1) von printf verwendet. Falls beliebig mehr als 0 bedeutet, wird die zweite Variante (2) verwendet. Das Funktions-Template (2) ist sehr interessant. Es kann eine beliebige Anzahl an Argumenten annehmen, solange diese größer als 0 ist. Das erste Argument wird an value gebunden und auf std::cout (3) geschrieben. Die verbleibenden Argumente werden in (4) verwendet, um einen rekursiven Aufruf (4) zu starten. Diese Rekursion erzeugt ein neues Funktions-Template myPrintf, das genau ein Argument weniger erwartet. Diese Rekursion geht gegen 0. Falls 0 eintritt, kommt als Abbruchbedingung die Funktion myPrintf (1) zum Zuge.

C++11- und C++14-Schulung

Ich freue mich darauf, Ihnen von 13. bis 15. März im Großraum Stuttgart modernes C++ in Theorie und Praxis genau vorstellen zu dürfen. Für die Schulung sind noch Plätze frei.

myPrintf ist typsicher, da die ganze Ausgabe von std::cout erledigt wird. Diese vereinfachte Variante unterstützt keine Formatangaben wie %d, %f oder %5.5f.

Es gibt viel über Ausdrücke zu erzählen. Die C++ Core Guidelines besitzt mehr als 25 Regeln für sie.