Probieren Sie jetzt C ++ 20 Contract Programming aus


In C ++ 20 wurde die Vertragsprogrammierung angezeigt. Bisher hat noch kein Compiler die Unterstützung für diese Funktion implementiert.


Es gibt jetzt jedoch eine Möglichkeit, Verträge aus C ++ 20 zu verwenden, wie im Standard beschrieben.


TL; DR


Es gibt ein Gabelgeräusch, das Verträge unterstützt. Anhand seines Beispiels erkläre ich Ihnen, wie Sie Verträge verwenden, damit Sie sofort mit der Verwendung einer Funktion in Ihrem bevorzugten Compiler beginnen können.


Es wurde bereits viel über Vertragsprogrammierung geschrieben, aber kurz gesagt, ich werde Ihnen sagen, was es ist und wofür es ist.


Logik von Hoar


Das Paradigma der Verträge basiert auf der Hoar-Logik ( 1 , 2 ).


Hoar-Logik ist ein Weg, um die Richtigkeit eines Algorithmus formal zu beweisen.
Es arbeitet mit Konzepten wie Vorbedingung, Nachbedingung und Invariante.
Aus praktischer Sicht ist die Verwendung der Hoar-Logik zum einen eine Möglichkeit, die Richtigkeit eines Programms in Fällen, in denen Fehler zu Katastrophen oder zum Tod führen können, formal zu beweisen. Zweitens eine Möglichkeit, die Zuverlässigkeit des Programms zusammen mit statischen Analysen und Tests zu erhöhen.


Vertragsprogrammierung


( 1 , 2 )


Die Hauptidee von Verträgen besteht darin, dass in Analogie zu Geschäftsverträgen Vereinbarungen für jede Funktion oder Methode beschrieben werden. Diese Vorkehrungen müssen sowohl vom Anrufer als auch vom Anrufer beachtet werden.
Ein wesentlicher Bestandteil von Verträgen sind mindestens zwei Montagemodi - Debugging und Lebensmittelgeschäft. Verträge sollten sich je nach Erstellungsmodus unterschiedlich verhalten. Am häufigsten werden Verträge in der Debug-Assembly überprüft und im Lebensmittelgeschäft ignoriert.


Manchmal werden Verträge auch in der Produktmontage geprüft und ihre Nichterfüllung kann beispielsweise zur Entstehung einer Ausnahme führen.


Der Hauptunterschied zwischen der Verwendung von Verträgen aus dem „klassischen“ Ansatz besteht darin, dass der Anrufer die im Vertrag beschriebenen Voraussetzungen des Angerufenen erfüllen muss und der Anrufer seine Nachbedingungen und Invarianten einhalten muss.
Dementsprechend ist der angerufene Teilnehmer nicht verpflichtet, die Richtigkeit seiner Parameter zu überprüfen. Diese Verpflichtung wird dem Anrufer vertraglich übertragen.


Die Nichteinhaltung von Verträgen sollte in der Testphase festgestellt werden und ergänzt alle Arten von Tests: modulare Integration usw.


Auf den ersten Blick erschwert die Verwendung von Verträgen die Entwicklung und beeinträchtigt die Lesbarkeit des Codes. In der Tat ist das genaue Gegenteil der Fall. Anhänger der statischen Typisierung können die Vorteile von Verträgen am einfachsten bewerten, da ihre einfachste Option darin besteht, Typen in der Signatur von Methoden und Funktionen zu beschreiben.


Was sind die Vorteile von Verträgen:


  • Verbessern Sie die Lesbarkeit des Codes durch explizite Dokumentation.
  • Verbessern Sie die Codezuverlässigkeit durch ergänzende Tests.
  • Ermöglichen Sie Compilern, Optimierungen auf niedriger Ebene zu verwenden und schnelleren Code basierend auf der Einhaltung von Verträgen zu generieren. Im letzteren Fall kann die Nichteinhaltung des Vertrags in der Freigabemontage zu UB führen.

Vertragsprogrammierung in C ++


Die Vertragsprogrammierung ist in vielen Sprachen implementiert. Die auffälligsten Beispiele sind Eiffel , wo das Paradigma zuerst implementiert wurde, und D in D-Verträgen sind Teil der Sprache.


In C ++ konnten Verträge vor dem C ++ 20-Standard als separate Bibliotheken verwendet werden.


Dieser Ansatz hat mehrere Nachteile:


  • Sehr ungeschickte Syntax mit Makros.
  • Das Fehlen eines einzigen Stils.
  • Unfähigkeit, Verträge des Compilers zur Optimierung des Codes zu verwenden.

Bibliotheksimplementierungen basieren normalerweise auf der Verwendung der guten alten Assert- und Präprozessor-Direktiven, die nach dem Kompilierungsflag suchen.


Die Verwendung von Verträgen in dieser Form macht den Code wirklich hässlich und unlesbar. Dies ist einer der Gründe, warum die Verwendung von Verträgen in C ++ wenig praktiziert wird.


Mit Blick auf die Zukunft werde ich zeigen, wie die Verwendung von Verträgen in C ++ 20 aussehen wird.
Und dann werden wir das alles genauer analysieren:


int f(int x, int y) [[ expects: x > 0 ]] // precondition [[ expects: y > 0 ]] // precondition [[ ensures r: r < x + y ]] // postcondition { int z = (x - x%y) / y; [[ assert: z >= 0 ]]; // assertion return z + y; } 

Versuchen Sie es


Leider hat derzeit noch keiner der weit verbreiteten Compiler eine Vertragsunterstützung implementiert.
Aber es gibt einen Ausweg.


Die ARCOS-Forschungsgruppe der Universidad Carlos III de Madrid implementierte experimentelle Unterstützung für Verträge in der Clang ++ - Gabel.


Um nicht „Code auf ein Blatt Papier zu schreiben“, sondern sofort neue Geschäftsmöglichkeiten ausprobieren zu können, können wir diese Gabel sammeln und damit die folgenden Beispiele ausprobieren.


Montageanleitungen sind in der Readme-Datei des Github-Repositorys beschrieben
https://github.com/arcosuc3m/clang-contracts


 git clone https://github.com/arcosuc3m/clang-contracts/ mkdir -p clang-contracts/build/ && cd clang-contracts/build/ cmake -G "Unix Makefiles" -DLLVM_USE_LINKER=gold -DBUILD_SHARED_LIBS=ON -DLLVM_USE_SPLIT_DWARF=ON -DLLVM_OPTIMIZED_TABLEGEN=ON ../ make -j8 

Ich hatte während der Montage keine Probleme, aber das Kompilieren der Quellen dauert sehr lange.


Um die Beispiele zu kompilieren, müssen Sie den Pfad zur Clang ++ - Binärdatei explizit angeben.
Zum Beispiel sieht es für mich so aus


 /home/valmat/work/git/clang-contracts/build/bin/clang++ -std=c++2a -build-level=audit -g test.cpp -o test.bin 

Ich habe Beispiele vorbereitet, damit Sie Verträge anhand von Beispielen mit echtem Code prüfen können. Ich schlage vor, bevor Sie mit dem Lesen des nächsten Abschnitts beginnen, Beispiele zu klonen und zu kompilieren.


 git clone https://github.com/valmat/cpp20-contracts-examples/ cd cpp20-contracts-examples make CPP=/path/to/clang++ 

Hier ist /path/to/clang++ Pfad zur clang++ Binärdatei Ihrer experimentellen Compiler-Assembly.


Zusätzlich zum Compiler selbst hat die ARCOS-Forschungsgruppe ihre Version des Compiler-Explorers für ihre Abzweigung vorbereitet.


Vertragsprogrammierung in C ++ 20


Jetzt hindert uns nichts mehr daran, die Möglichkeiten der Vertragsprogrammierung zu erforschen und diese Möglichkeiten sofort in der Praxis auszuprobieren.


Wie oben erwähnt, werden Verträge aus Vorbedingungen, Nachbedingungen und Invarianten (Aussagen) aufgebaut.


In C ++ 20 werden hierfür Attribute mit folgender Syntax verwendet


 [[contract-attribute modifier identifier: conditional-expression]] 

Wobei das contract-attribute einen der folgenden Werte annehmen kann:
erwartet , sichert oder behauptet .


expects für Vorbedingungen verwendet, ensures für Nachbedingungen ensures und assert für Aussagen.


conditional-expression ist ein boolescher Ausdruck, der in einem Vertragsprädikat validiert wird.
modifier und identifier können weggelassen werden.


Warum brauche ich einen modifier Ich werde etwas tiefer schreiben.


identifier nur mit ensures und dient zur Darstellung des Rückgabewerts.


Voraussetzungen haben Zugriff auf Argumente.


Nachbedingungen haben Zugriff auf den von der Funktion zurückgegebenen Wert. Hierfür wird die Syntax verwendet.


 [[ensures return_variable: expr(return_variable)]] 

Wobei return_variable ein gültiger Ausdruck für die Variable ist.


Mit anderen Worten, Vorbedingungen sollen Einschränkungen deklarieren, die Argumenten auferlegt werden, die von der Funktion akzeptiert werden, und Nachbedingungen, um Einschränkungen zu deklarieren, die dem von der Funktion zurückgegebenen Wert auferlegt werden.


Es wird angenommen, dass Vor- und Nachbedingungen Teil der Funktionsschnittstelle sind, während Anweisungen Teil ihrer Implementierung sind.


Voraussetzungsprädikate werden immer unmittelbar vor der Ausführung der Funktion ausgewertet. Die Nachbedingungen sind unmittelbar nach Übergabe der Steuerfunktion an den aufrufenden Code erfüllt.


Wenn in einer Funktion eine Ausnahme ausgelöst wird, wird die Nachbedingung nicht überprüft.
Nachbedingungen werden nur geprüft, wenn die Funktion normal abgeschlossen ist.


Wenn beim Überprüfen des Ausdrucks im Vertrag eine Ausnahme aufgetreten ist, wird std::terminate() aufgerufen.


Vor- und Nachbedingungen werden immer außerhalb des Funktionskörpers beschrieben und können nicht auf lokale Variablen zugreifen.


Wenn Vor- und Nachbedingungen einen Vertrag für eine öffentliche Klassenmethode beschreiben, können sie nicht auf private und geschützte Klassenfelder zugreifen. Wenn die Klassenmethode geschützt ist, besteht Zugriff auf die geschützten und öffentlichen Daten der Klasse, jedoch nicht auf private.
Die letzte Einschränkung ist völlig logisch, da der Vertrag Teil der Methodenschnittstelle ist.


Anweisungen (Invarianten) werden immer im Hauptteil einer Funktion oder Methode beschrieben. Sie sind von Natur aus Teil der Implementierung. Dementsprechend können sie auf alle verfügbaren Daten zugreifen. Einschließlich lokaler Funktionsvariablen sowie privater und geschützter Klassenfelder.


Beispiel 1


Wir definieren zwei Voraussetzungen, eine Nachbedingung und eine Invariante:


 int foo(int x, int y) [[ expects: x > y ]] // precondition #1 [[ expects: y > 0 ]] // precondition #2 [[ ensures r: r < x ]] // postcondition #3 { int z = (x - x%y) / y; [[ assert: z >= 0 ]]; // assertion return z; } int main() { std::cout << foo(117, 20) << std::endl; std::cout << foo(10, 20) << std::endl; // <-- contract violation #1 std::cout << foo(100, -5) << std::endl; // <-- contract violation #2 return 0; } 

Beispiel 2


Eine Voraussetzung einer öffentlichen Methode kann sich nicht auf ein geschütztes oder privates Feld beziehen:


 struct X { //protected: int m = 5; public: int foo(int n) [[expects: n < m]] { return n*n; } }; 

Änderungen von Variablen innerhalb der durch die Vertragsattribute beschriebenen Ausdrücke sind nicht zulässig. Wenn es kaputt ist, wird es UB geben.


Die in den Verträgen beschriebenen Ausdrücke sollten keine Nebenwirkungen haben. Obwohl Compiler dies überprüfen können, müssen sie dies nicht tun. Verstöße gegen diese Anforderung gelten als undefiniertes Verhalten.


 struct X { int m = 5; int foo(int n) [[ expects: n < m++ ]] // UB: Modifies variable m { int k = n*n; [[ assert: ++k < 100 ]] // UB: Modifies variable k return n*n; } }; 

Die Anforderung, den Status des Programms in Vertragsausdrücken nicht zu ändern, wird etwas geringer, wenn ich über die Ebenen der Vertragsmodifikatoren und Erstellungsmodi spreche.


Jetzt stelle ich nur fest, dass das richtige Programm so funktionieren sollte, als gäbe es überhaupt keine Verträge.


Wie oben erwähnt, können Sie im Vertrag so viele Vor- und Nachbedingungen angeben, wie Sie möchten.
Alle werden der Reihe nach überprüft. Die Vorbedingungen werden jedoch immer vor der Ausführung der Funktion und unmittelbar nach dem Beenden der Funktion überprüft.


Dies bedeutet, dass die Voraussetzungen immer zuerst überprüft werden, wie im folgenden Beispiel dargestellt:


 int foo(int n) [[ expects: expr(n) ]] // # 1 [[ ensures r: expr(r) ]] // # 4 [[ expects: expr(n) ]] // # 2 [[ expects: expr(n) ]] // # 3 [[ ensures r: expr(r) ]] // # 5 {...} 

Ausdrücke in Nachbedingungen können sich nicht nur auf den von der Funktion zurückgegebenen Wert beziehen, sondern auch auf die Argumente der Funktion.


 int foo(int &n) [[ ensures: expr(n) ]]; 

In diesem Fall können Sie die Rückgabewert-ID weglassen.


Wenn sich die Nachbedingung auf das Argument der Funktion bezieht, wird dieses Argument am Austrittspunkt der Funktion und nicht am Einstiegspunkt berücksichtigt, wie dies bei Vorbedingungen der Fall ist.


Es gibt keine Möglichkeit, in der Nachbedingung auf den ursprünglichen Wert (am Funktionseintrittspunkt) zu verweisen.


Beispiel :


 void incr(int &n) [[ expects: 3 == n ]] [[ ensures: 4 == n ]] {++n;} 

Prädikate in Verträgen können nur dann auf lokale Variablen verweisen, wenn die Lebensdauer dieser Variablen der Prädikatenberechnungszeit entspricht.


Beispielsweise können für constexpr Funktionen nur dann auf lokale Variablen verwiesen werden, wenn sie zur Kompilierungszeit bekannt sind.


Beispiel :


 int a = 1; constexpr int b = 100; constexpr int foo(int n) [[ expects: a <= n ]] // error: `a` is not constexpr [[ expects: n < b ]] // OK { [[assert: n > 2*a]]; // error: `a` is not constexpr [[assert: n < 2*b]]; // OK return 2*n; } 

Verträge für Funktionszeiger


Sie können keine Verträge für einen Funktionszeiger definieren, aber Sie können einem Funktionszeiger die Adresse einer Funktion zuweisen, für die ein Vertrag definiert ist.


Beispiel :


 int foo(int n) [[expects: n < 10]] { return n*n; } int (*pfoo)(int n) = &foo; 

Das Aufrufen von pfoo(100) verstößt gegen den Vertrag.


Erbverträge


Die klassische Umsetzung des Vertragskonzepts legt nahe, dass Vorbedingungen in Unterklassen geschwächt, Nachbedingungen und Invarianten in Unterklassen gestärkt werden können.


In einer C ++ 20-Implementierung ist dies nicht der Fall.


Erstens sind Invarianten in C ++ 20 Teil einer Implementierung und keine Schnittstelle. Aus diesem Grund können sie sowohl gestärkt als auch geschwächt werden. Wenn die Implementierung der virtuellen Funktion keine assert , wird sie nicht vererbt.


Zweitens ist es erforderlich, dass beim Erben der Funktionen ODR identisch sind.
Und da Vor- und Nachbedingungen Teil der Schnittstelle sind, müssen sie im Erben genau übereinstimmen.


Darüber hinaus kann auf die Beschreibung von Vor- und Nachbedingungen während der Vererbung verzichtet werden. Wenn sie jedoch deklariert sind, müssen sie genau mit der Definition in der Basisklasse übereinstimmen.


Beispiel :


 struct Base { virtual int foo(int n) [[ expects: n < 10 ]] [[ ensures r: r > 100 ]] { return n*n; } }; struct Derived1 : Base { virtual int foo(int n) override [[ expects: n < 10 ]] [[ ensures r: r > 100 ]] { return n*n*2; } }; struct Derived2 : Base { // Inherits contracts from Base virtual int foo(int n) override { return n*3; } }; 

Bemerkung

Leider funktioniert das obige Beispiel im experimentellen Compiler nicht wie erwartet.


Wenn foo von Derived2 Vertrag Derived2 , wird er nicht von der Basisklasse geerbt. Darüber hinaus können Sie mit dem Compiler für eine Unterklasse einen Vertrag ermitteln, der nicht mit dem Basisvertrag übereinstimmt.


Ein weiterer experimenteller Compilerfehler:


Der Datensatz sollte syntaktisch korrekt sein


 virtual int foo(int n) override [[expects: n < 10]] {...} 

In dieser Form habe ich jedoch einen Kompilierungsfehler erhalten


 inheritance1.cpp:20:36: error: expected ';' at end of declaration list virtual int foo(int n) override ^ ; 

und musste ersetzt werden durch


 virtual int foo(int n) [[expects: n < 10]] override {...} 

Ich denke, dies liegt an der Besonderheit des experimentellen Compilers, und syntaxkorrekter Code funktioniert in den Release-Versionen von Compilern.


Vertragsmodifikatoren


Vertragsprädikatprüfungen können zusätzliche Verarbeitungskosten verursachen.
Daher ist es üblich, Verträge in Entwicklungs- und Test-Builds zu überprüfen und sie im Release-Build zu ignorieren.


Für diese Zwecke bietet der Standard drei Ebenen von Vertragsmodifikatoren. Mit Modifikatoren und Compilertasten kann der Programmierer steuern, welche Kontakte in der Baugruppe geprüft und welche ignoriert werden.


  • default - Dieser Modifikator wird standardmäßig verwendet. Es wird angenommen, dass die Berechnungskosten für die Überprüfung der Ausführung eines Ausdrucks mit diesem Modifikator im Vergleich zu den Kosten für die Berechnung der Funktion selbst gering sind .
  • audit - Dieser Modifikator geht davon aus, dass der Rechenaufwand für die Überprüfung der Ausführung eines Ausdrucks im Vergleich zu den Kosten für die Berechnung der Funktion selbst erheblich ist.
  • axiom - Dieser Modifikator wird verwendet, wenn der Ausdruck deklarativ ist. Zur Laufzeit nicht überprüft. Dient zur Dokumentation der Schnittstelle einer Funktion zur Verwendung durch statische Analysatoren und einen Compiler-Optimierer. Ausdrücke mit dem axiom Modifikator axiom zur Laufzeit niemals ausgewertet.

Beispiel


 [[expects: expr]] //  default [[expects default: expr]] //  default [[expects axiom : expr]] // Run-time    [[expects audit : expr]] //    

Mithilfe von Modifikatoren können Sie bestimmen, welche Überprüfungen in welchen Versionen Ihrer Assemblys verwendet und welche deaktiviert werden.


Es ist anzumerken, dass der Compiler das Recht hat, den Vertrag auch dann für Optimierungen auf niedriger Ebene zu verwenden, wenn die Prüfung nicht durchgeführt wird. Obwohl die Vertragsüberprüfung durch das Kompilierungsflag deaktiviert werden kann, führt eine Vertragsverletzung zu einem undefinierten Programmverhalten.


Nach Ermessen des Compilers können Einrichtungen bereitgestellt werden, die die axiom als axiom gekennzeichneten Ausdrücken ermöglichen.


In unserem Fall ist dies eine Compileroption


 -axiom-mode=<mode> 

-axiom-mode=on schaltet den Axiom-Modus ein und -axiom-mode=on dementsprechend die Überprüfung von Ansprüchen mit dem Bezeichner- axiom .


-axiom-mode=off schaltet den Axiom-Modus aus und ermöglicht dementsprechend die Überprüfung von Anweisungen mit dem Bezeichner- axiom .


Beispiel :


 int foo(int n) [[expects axiom: n < 10]] { return n*n; } 

Ein Programm kann mit drei verschiedenen Überprüfungsebenen zusammengestellt werden:


  • off schaltet alle Ausdrucksprüfungen in Verträgen aus
  • default nur Ausdrücke mit dem default
  • Erweiterten Überwachungsmodus, wenn alle Überprüfungen mit dem default und dem Überwachungsmodifikator durchgeführt werden

Wie genau die Installation der Verifizierungsstufe implementiert wird, liegt im Ermessen der Compiler-Entwickler.


In unserem Fall wird hierfür die Compileroption verwendet


 -build-level=<off|default|audit> 

Der Standardwert ist -build-level=default


Wie bereits erwähnt, kann der Compiler Verträge für Optimierungen auf niedriger Ebene verwenden. Aus diesem Grund führt ihre Nichterfüllung zu undefiniertem Verhalten, obwohl zum Zeitpunkt der Ausführung einige Prädikate in den Verträgen (abhängig vom Überprüfungsgrad) möglicherweise nicht berechnet werden.


Ich werde Beispiele für die Anwendung von Baugruppenebenen auf den nächsten Abschnitt verschieben, wo sie visuell dargestellt werden können.


Abfangen von Vertragsverletzungen


Je nachdem, welche Optionen das Programm bietet, kann es bei Vertragsbruch zu unterschiedlichen Verhaltensszenarien kommen.


Standardmäßig führt eine Vertragsverletzung zum Absturz des Programms und zum Aufruf von std::terminate() . Der Programmierer kann dieses Verhalten jedoch außer Kraft setzen, indem er seinen eigenen Handler bereitstellt und dem Compiler anzeigt, dass das Programm nach Vertragsbruch fortgesetzt werden muss.


Bei der Kompilierung können Sie den Verstoßbehandler installieren, der aufgerufen wird, wenn der Vertrag verletzt wird.


Die Implementierung der Installation des Handlers liegt im Ermessen der Ersteller des Compilers.


In unserem Fall dies


 -contract-violation-handler=<violation_handler> 

Die Prozessorsignatur sollte sein


 void(const std::contract_violation& info) 

oder


 void(const std::contract_violation& info) noexcept 

std::contract_violation entspricht der folgenden Definition:


 struct contract_violation { uint_least32_t line_number() const noexcept; std::string_view file_name() const noexcept; std::string_view function_name() const noexcept; std::string_view comment() const noexcept; std::string_view assertion_level() const noexcept; }; 

Auf diese Weise erhalten Sie mit dem Handler umfassende Informationen darüber, wo und unter welchen Bedingungen eine Vertragsverletzung aufgetreten ist.


Wenn der Handler für die Verletzung von Handlern angegeben ist, wird im Falle einer Vertragsverletzung standardmäßig std::abort() sofort nach seiner Ausführung aufgerufen (ohne Angabe des Handlers wird std::terminate() aufgerufen).


Der Standard geht davon aus, dass Compiler Tools bereitstellen, mit denen Programmierer ein Programm nach Vertragsbruch weiter ausführen können.


Die Implementierung dieser Tools liegt im Ermessen der Compiler-Entwickler.
In unserem Fall ist dies eine Compileroption


 -fcontinue-after-violation 

Die -fcontinue-after-violation und -contract-violation-handler können unabhängig voneinander festgelegt werden. Beispielsweise können Sie -fcontinue-after-violation -contract-violation-handler , aber nicht -contract-violation-handler . Im letzteren Fall funktioniert das Programm nach einer Vertragsverletzung einfach weiter.


Die Möglichkeit, das Programm nach einer Vertragsverletzung fortzusetzen, ist im Standard festgelegt. Diese Funktion muss jedoch mit Vorsicht angewendet werden.


Technisch gesehen ist das Verhalten eines Programms nach Vertragsbruch nicht definiert, auch wenn der Programmierer ausdrücklich angegeben hat, dass das Programm weiter funktionieren soll.


Dies liegt daran, dass der Compiler Optimierungen auf niedriger Ebene basierend auf der Vertragsausführung durchführen kann.


Im Idealfall müssen Sie bei Vertragsbruch Diagnoseinformationen so schnell wie möglich aufzeichnen und das Programm beenden. Sie müssen genau verstehen, was Sie tun, indem Sie zulassen, dass das Programm nach einem Verstoß funktioniert.


Definieren Sie Ihren Handler und verwenden Sie ihn, um eine Vertragsverletzung abzufangen


 void violation_handler(const std::contract_violation& info) { std::cerr << "line_number : " << info.line_number() << std::endl; std::cerr << "file_name : " << info.file_name() << std::endl; std::cerr << "function_name : " << info.function_name() << std::endl; std::cerr << "comment : " << info.comment() << std::endl; std::cerr << "assertion_level : " << info.assertion_level() << std::endl; } 

Betrachten Sie ein Beispiel für eine Vertragsverletzung:


 #include "violation_handler.h" int foo(int n) [[expects: n < 10]] { return n*n; } int main() { foo(100); // <-- contract violation return 0; } 

Wir kompilieren das Programm mit den Optionen -contract-violation-handler=violation_handler -fcontinue-after-violation und -fcontinue-after-violation und führen es aus


 $ bin/example8-handling.bin line_number : 4 file_name : example8-handling.cpp function_name : foo comment : n < 10 assertion_level : default 

Jetzt können wir Beispiele geben, die das Verhalten des Programms bei Vertragsbruch auf verschiedenen Montageebenen und Vertragsmodi demonstrieren.


Betrachten Sie das folgende Beispiel :


 #include "violation_handler.h" int foo(int n) [[ expects axiom : n < 100 ]] [[ expects default : n < 200 ]] [[ expects audit : n < 300 ]] { return 2 * n; } int main() { foo(350); // audit foo(250); // default return 0; } 

Wenn Sie es mit der Option -build-level=off , werden die Verträge erwartungsgemäß nicht überprüft.


-build-level=default wir die default (mit der Option -build-level=default ) -build-level=default , erhalten wir die folgende Ausgabe:


 $ bin/example9-default.bin line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default 

Und die Versammlung mit der audit Ebene wird geben:


  $ bin/example9-audit.bin line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default line_number : 6 file_name : example9.cpp function_name : foo comment : n < 300 assertion_level : audit line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default 

Bemerkungen


violation_handler kann Ausnahmen auslösen. In diesem Fall können Sie das Programm so konfigurieren, dass eine Vertragsverletzung zum Auslösen einer Ausnahme führt.


Wenn die Funktion, deren Verträge beschrieben sind, als noexcept markiert noexcept und bei der Überprüfung der Vertragsverletzung_handler aufgerufen wird, was eine Ausnahme noexcept , wird std::terminate() aufgerufen.


Beispiel


 void violation_handler(const std::contract_violation&) { throw std::exception(); } int foo(int n) noexcept [[ expects: n > 0 ]] { return n*n; } int main() { foo(0); // <-- std::terminate() when violation handler throws an exception return 0; } 

Wenn das Flag an den Compiler übergeben wird: Führen Sie das Programm nach Vertragsbruch nicht weiter aus ( continuation mode=off ), aber der Verstoßbehandler löst eine Ausnahme aus, dann wird std::terminate() erzwungen.


Fazit


Verträge beziehen sich auf nicht aufdringliche Laufzeitprüfungen. Sie spielen eine sehr wichtige Rolle bei der Sicherstellung der Qualität der veröffentlichten Software.


C ++ wird sehr häufig verwendet. Und mit Sicherheit wird es eine ausreichende Anzahl von Ansprüchen auf die Spezifikation von Verträgen geben. Meiner subjektiven Meinung nach erwies sich die Implementierung als sehr praktisch und visuell.


C ++ 20-Verträge machen unsere Programme noch zuverlässiger, schneller und verständlicher. Ich freue mich auf ihre Implementierung in Compilern.




PS
In PM sagen sie mir, dass wahrscheinlich in der endgültigen Version des Standards expects und ensures wird ensures durch pre bzw. post ersetzt zu werden.

Source: https://habr.com/ru/post/de443766/


All Articles