Entwicklung & Code

Contracts in C++ 26: Ein tiefer Einblick in die Verträge


Ich habe Contracts schon in einem früheren Blogbeitrag vorgestellt. In zwei weiteren Artikeln gehe ich nun näher auf die Details ein.




Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

In diesem Beitrag beziehe ich mich hauptsächlich auf das exzellente Proposal „Contracts for C++“. Leider ist der Proposal P2900R14 über 100 Seiten lang. Daher werde ich versuchen, die wichtigsten Punkte kurz und prägnant zusammenzufassen. Im Zweifelsfall sollten Interessierte aber das Proposal selbst lesen.

Ich möchte diesen Artikel mit ein paar grundlegenden Definitionen beginnen, die für ein tieferes Verständnis von Contracts wichtig sind.

Ein Contract legt Schnittstellen für Softwarekomponenten präzise und überprüfbar fest. Diese Softwarekomponenten sind in C++26 Funktionen und Methoden. Der Vertrag besteht aus einer Reihe von Bedingungen.

Eine Contract Violation liegt vor, wenn eine Bedingung des Contract nicht erfüllt ist. Eine Contract Violation kann aus drei Gründen auftreten:

  1. Die Auswertung des Prädikats gibt false zurück.
  2. Die Auswertung des Prädikats verursacht eine Ausnahme.
  3. Die Auswertung des Prädikats erfolgt zur Compile-Zeit, aber das Prädikat ist kein konstanter Ausdruck.

Ein korrektes Programm ist ein Programm, das nicht gegen die Contracts verstößt. Die Bedingungen lassen sich in drei Kategorien einteilen:

  1. Eine Precondition: ein Prädikat, das beim Eintritt in eine Funktion gelten soll.
  2. Eine Postcondition: Ein Prädikat, das beim Verlassen der Funktion gelten soll.
  3. Eine Invariant: Ein Prädikat, das an seiner Stelle in der Berechnung gelten soll.

Die Precondition und die Postcondition werden außerhalb der Funktionsdefinition platziert, die Invariant hingegen innerhalb der Funktionsdefinition. Ein Prädikat ist ein Ausdruck, der einen booleschen Wert zurückgibt. Diese Prädikate, die zur Überprüfung des Vertrags verwendet werden, heißen Contract Assertions.

In folgendem Beispiel aus dem Proposals steht contract_assert für die Invariante:


int f(const int x)
  pre (x != 1) // a precondition assertion
  post(r : r == x && r != 2) // a postcondition assertion; r names the result object of f
{
  contract_assert (x != 3); // an assertion statement
  return x;
}


Die folgenden Aufrufe der Funktion f zeigen die Vertragsverletzungen:


void g()
{
  f(0); // no contract violation
  f(1); // violates precondition assertion of f
  f(2); // violates postcondition assertion of f
  f(3); // violates assertion statement within f
  f(4); // no contract violation
}


Ein paar Einschränkungen gelten noch für Contracts. Im Moment können virtuelle Funktionen keine Preconditions oder Postconditions enthalten. Das wird sich wahrscheinlich mit zukünftigen Standards ändern.

Ähnliche Einschränkungen gelten für defaulted und deleted Funktionen. Die erste Deklaration dieser Funktionen darf keine Preconditions oder Postconditions enthalten. Natürlich gelten Einschränkungen auch für Konstruktoren und Destruktoren. Preconditions in Konstruktoren können nur auf nicht statische Datenelemente der Klasse über den this-Zeiger zugreifen. Das Gleiche gilt für Postconditions in Destruktoren.

Nun bleiben noch zwei wichtige Fragen zu beantworten:

  1. Wann werden Contract-Assertions ausgewertet?
  2. Wie ist die Semantik der Auswertung von Contract-Assertions?

Im Proposal heißt es: „Preconditions werden unmittelbar nach der Initialisierung der Funktionsparameter und vor dem Eintritt in den Funktionskörper ausgewertet. Postconditions werden unmittelbar nach dem Löschen der lokalen Variablen in der Funktion ausgewertet, wenn eine Funktion normal zurückkehrt. Assertionsanweisungen werden an der Stelle in der Funktion ausgeführt, an der der Kontrollfluss sie erreicht.“



Die Tabelle zeigt die Semantic für die Evaluierung.

(Bild: Rainer Grimm)

Das Proposal sieht vier Auswertungs-Semantiken vor: ignore, observe, enforce und quick-enforce. Eine Implementierung kann aber auch ihre eigene Semantik implementieren. Die Auswahl der Auswertungs-Semantik hängt von der Implementierung ab. Sie kann eine semantische Auswahl zur Compile-, Link-, Lade- oder Laufzeit bieten.

Die wahrscheinlichste Option ist die Auswahl über Compiler-Flags. Das folgende Flag wird für die Compiler Clang und GCC verwendet, um die Auswertungs-Semantik ignore anzuwenden: –fcontract_semantic=ignore. Es ist aber auch denkbar, dass die Auswahl über Konfigurationsdateien oder Hooks in der ausführbaren Datei erfolgt.

Standardmäßig sollte die Semantik zur Laufzeit erzwungen werden. Eine passende Standardkonfiguration für einen optimierten Release-Build sollte die Semantik zur Compilezeit erzwingen, aber zur Laufzeit ignorieren.

In meinem nächsten Artikel werde ich meine ausführliche Betrachtung von Contracts abschließen. Dabei werde ich insbesondere die vier Auswertungs-Semantiken ignore, observe, enforce und quick-enforce näher betrachten.


(rme)



Source link

Beliebt

Die mobile Version verlassen