C++ Core Guidelines: Profile

Modernes C++  –  0 Kommentare

Vereinfachend gesagt, sind Profile Teilemengen von Regeln der C++ Core Guidelines, die sich mit einem besonderen Aspekt beschäftigen. Er kann Type Safety, Bound Safety oder auch Lifetime Safety sein. Dank der Guidelines Support Library lassen sich diese Aspekte prüfen.

Es gibt zwei Gründe für die Profile.

  1. Du musst dich mit bestehendem Code auseinandersetzen und es ist daher nicht möglich, alle Regel in einem Schritt anzuwenden. In diesem Fall bietet es sich an, die Regeln schrittweise einzuführen.
  2. Manche Regeln können wichtiger für deine Codebasis sein als andere. Genau hier passen die Profile ins Bild, da sie spezifische Aspekte wie die Vermeidung von Zugriffsfehlern oder die richtige Verwendung von Datentypen adressieren. Regeln, die einen besonderen Aspekt im Fokus haben, werden Profile genannt.

Nun aber zu der formalen Definition eines Profils aus den C++ Core Guidelines:

  • Profile: A “profile” is a set of deterministic and portably enforceable subset rules (i.e., restrictions) that are designed to achieve a specific guarantee.

Zwei Begriffe der Definition sind besonders interessant:

  • deterministic: Die Probleme sollen sich lokal analysieren und in einem Compiler implementieren lassen.
  • portably enforcable: Verschiedene Werkzeuge auf verschieden Plattformen sollen das äquivalente Ergebnis erzeugen.

Nun stellt sich natürlich die Frage: Wann setzt dein Code ein Profil richtig um? Die Antwort ist einfach: Es darf keine Warnung mehr geben, die sich aufgrund dieses Profils ergeben. Die C++ Core Guidelines besitzt drei Profile:

Entsprechend der C++ Core Guidelines werden in Zukunft eventuell zusätzliche Profile definiert werden. Diese können sich mit Aspekten wie undefined und unspecified behaviour beschäftigen. Der Einfachheit halber werde ich von nun an von undefiniertem und unspezifizierten Verhalten sprechen. Bevor ich mich in meinem nächsten Artikel tiefer mit den Profilen beschäftige, möchte ich erst klären, worin der Unterschied zwischen undefinierten und unspezifizierten Verhalten besteht. Hier kommt mein kleiner Ausflug.

Undefiniertes und unspezifiziertes Verhalten

Dies sind die Definitionen der zwei Begriffe aus dem aktuellen C++20-Standard-Entwurf. Er ist in amerikanischen Englisch verfasst.

  • Undefined behavior (3.27): behavior for which this document imposes no requirements.
  • Unspecified behavior (3.28): behavior, for a well-formed program, construct and correct data, that depends on the implementation.

Diese Begriffe muss ich genauer erklären:

Undefiniertes Verhalten

Vereinfachend formuliert besagt undefiniertes Verhalten für ein Programm, dass keine verlässlichen Aussagen zu einem Programm mehr möglich sind. Das Ergebnis des Programms kann daher das vermeintlich erwartete sein, es kann aber auch ein falsches Ergebnis ausgeben, die Kompilierung des Programms kann aber auch scheitern oder es gibt einen Laufzeitfehler. Das Verhalten kann von der Plattform, dem Compiler, der Compilerversion, dem Optimierungslevel oder einfach nur dem Zustand des Computers abhängen. Diese Aufzählung ließe sich noch deutlich verlängern; ich höre aber aus gutem Grunde auf. Die Aktion, die im Falle eines undefined behaviours ansteht, ist hingegen sehr einfach: Beseitige das undefinierte Verhalten!

Hier ist eine Aufzählung von typischen undefinierten Verhalten:

  • Zugriff auf die Elemente eines C-Arrays oder eines Containers der STL jenseits seiner Grenzen
  • Verwendung von nichtinitialisierten Variablen
  • Dereferenzieren eines Null-Zeigers
  • Teilen durch null
  • Undefined order of evaluation (Nichtdefinierte Reihenfolge von Auswertungen)

Abgesehen vom letzten Punkt sollte die Aufzählung verschiedener undefinierter Verhalten offensichtlich sein.

Informell ausgedrückt bedeutet "undefined order of evaluation" eines Ausdrucks A, der von einem Teil B gefolgt wird, dass der Compiler A und B in einer beliebigen Reihenfolge auswerten kann. Nun benötigen wir die Zusicherung, dass A vor B ausgewertet wird (sequenced_before). Diese Zusicherung oder auch Etablierung einer sequenced_before-Relation geben zum Beispiel logische Operatoren, vollständige Ausdrücke (a = c;), der Aufruf oder das Verlassen einer Funktion oder auch die Initialisierung einer Variable. Zugegeben, dies war eine vereinfachte Vorstellung der "order of evaluation". Mehr Details gibt es auf cppreference.com.

Das kleine Programm sollte die Begrifflichkeit auf den Punkt bringen. Wenn ich das folgende Programm mit C++14 ausführe, erhalte ich drei Warnungen:

// undefinedBehaviour.cpp

#include <array>
#include <iostream>

int main(){

std::cout << std::endl;

std::array<int, 1> myArr{}; // (0)

int i{}; // (0)

myArr[i] = i++; // (1)

std::cout << i << " " << i++ << std::endl; // (1)

std::cout << std::endl;

int n = ++i + i; // (2)

std::cout << "n: " << n << std::endl;

std::cout << std::endl;

}

Ich verwende in der Zeile (0) geschweifte Klammern, um beide Datentypen zu initialisieren. Die drei Ausdrücke in den Zeilen (1) und (2) besitzen mit C++14 undefiniertes Verhalten. Der Grund ist, das die Ausdrücke erst vollständig am Ende des Ausdrucks vollständig ausgewertet sein müssen. Das Ende des Ausdrucks ist in diesem Fall der Strickpunkt. Der Clang-Compiler bringt dies unmissverständlich mit einer Warnung auf den Punkt.

"Unsequend Evaluation" bedeutet, dass die Operation in einer beliebigen Reihenfolge ausgeführt werden und sich sogar überlappen können. Dies ist selbst in einer Single-threaded-Ausführung möglich, denn die zugrunde liegenden Assemblerinstruktionen können sich überlappen. Das war jedoch noch nicht die ganze Wahrheit. Die Zeilen (1) besitzen undefiniertes Verhalten in C++14, aber unspezifiziertes Verhalten in C++17.

Unspezifiziertes Verhalten

Unspezifiziertes Verhalten bedeutet, dass die Implementierung nicht dokumentieren muss, wie sich diese in diesem Kontext verhält. Zum Beispiel ist es nicht spezifiziert, in welcher Reihenfolge die Argumente eines Funktionsaufrufs ausgewertet werden.

Hierzu habe ich ein interessantes Beispiel, dass in C++14 undefiniertes Verhalten und in C++17 unspezifiziertes Verhalten besitzt:

#include <iostream>

void func(int fir, int sec){
std::cout << "(" << fir << "," << sec << ")" << std::endl;
}

int main(){
int i = 0;
func(i++, i++);
}

Wenn ich das Programm ausführe, erhalten ich verschiedene Ergebnisse mit dem GCC und dem Clang-Compiler. Ich erhalte also weder das gleiche Ergebnis noch findet die Auswertung der Funktionsargumente von links nach rechts statt.

  • GCC
  • Clang

Unspezifiziertes Verhalten mit C++17 gibt dir die Zusicherung, dass jedes Element zuerst vollständig ausgewertet wird, bevor das nächste Argument ausgewertet wird. Es gibt aber noch immer keine Zusicherung, in welcher Reihenfolge die Argumente ausgewertet werden.

Wie geht's weiter?

Nach diesem notwendigen Umweg zu undefiniertem und unspezifizierten Verhalten werde ich in meinem nächsten Artikel über die drei Profile type safety, bounds safety und lifetime safety schreiben.

Die nächsten pdf-Päckchen stehen fest:

Die Details zur Wahl stehen fest. Die PDF-Päckchen gibt es in ein bis zwei Wochen.