Softwareentwicklung: Contracts in C++26
Contracts sind ein wichtiges Feature, das Bedingungen für Funktionen definiert und eigentlich schon in C++20 landen sollte.
- Rainer Grimm
Nach der Kurzserie zu Reflection in C++26 geht es nun um Contracts. Damit kann man Preconditions, Postconditions und Invariants für Funktionen angeben.
Contracts sollten bereits Teil von C++20 sein, wurden aber in der Standardbesprechung in Köln entfernt. Herb Sutter sagte auf Sutter's Mill Folgendes darüber: „Contracts is the most impactful feature of C++20 so far, and arguably the most impactful feature we have added to C++ since C++11.” Mit C++26 bekommen wir sie wahrscheinlich.
Dieser Artikel basiert auf dem Proposal P2961R2.
Was ist ein Contract?
Ein Contract legt Schnittstellen für Softwarekomponenten auf präzise und überprüfbare Weise fest. Diese Softwarekomponenten sind Funktionen und Methoden, die Precondition, Postconditions und Invariants erfüllen müssen. Hier sind die Definitionen:
- Eine Precondition: ein Prädikat, das beim Eintritt in eine Funktion gelten soll,
- Eine Postcondition: ein Prädikat, das beim Verlassen der Funktion gelten soll,
- Eine Assertion: ein Prädikat, das an seinem Punkt in der Berechnung gelten soll.
Die Precondition und die Postcondition werden außerhalb der Funktionsdefinition platziert, die Invariante jedoch innerhalb der Funktionsdefinition. Ein Prädikat ist ein Ausdruck, der einen Booleschen Wert zurückgibt.
Bevor ich euch das erste Beispiel zeige, möchte ich etwas über die Ziele des Vertragsdesigns schreiben.
Designziele
Im Englischen sind folgende Designziele definiert:
- The syntax should fit naturally into existing C++. The intent should be intuitively understandable by users unfamiliar with contract checks without creating any confusion.
- A contract check should not resemble an attribute, a lambda, or any other pre-existing C++ construct. It should sit in its own, instantly recognisable design space.
- The syntax should feel elegant and lightweight. It should not use more tokens and character than necessary.
- To aid readability, the syntax should visually separate the different syntactic parts of a contract check. It should be possible to distinguish at a glance the contract kind, the predicate, the name for the return value … (Proposal P2961R2)
Nun folgt das erste Beispiel:
int f(int i)
pre (i >= 0)
post (r: r > 0)
{
contract_assert (i >= 0);
return i+1;
}
pre
oder post
- fügen eine Precondition beziehungsweise Postcondition hinzu. Eine Funktion kann eine beliebige Anzahl von Preconditions und Postconditions haben. Sie können beliebig miteinander vermischt werden.
- sind kontextabhängige Schlüsselwörter, also in bestimmten Kontexten ein Schlüsselwort, aber ein Bezeichner außerhalb dieses Kontextes.
- sind am Ende der Funktionsdeklaration positioniert.
post
kann einen Rückgabewert haben. Der Bezeichner muss vor dem Prädikat stehen, gefolgt von einem Doppelpunkt.
contract_assert
- ist ein Schlüsselwort. Andernfalls könnte es nicht von einem Funktionsaufruf unterschieden werden.
Das Assertion-Problem
Das ideale Schlüsselwort für die Zusicherung wäre assert
, aber nicht contract_assert. assert
wird in den meisten Programmiersprachen verwendet, um vertragsähnliche Behauptungen auszudrücken. Aber C++ hat ein Legacy-Problem.
#include <cassert>
void f() {
int i = get_i();
assert(i >= 0); // identical syntax for contract assert and macro assert!
use_i(i);
}
assert
ist bereits ein Makro aus dem Header <cassert
>.
Verletzung des Contracts
Wird der Contract verletzt, führt dies zu einem Laufzeitfehler.
// contract.cpp
#include <iostream>
int f(int i)
pre (i >= 0)
post (r: r > 0)
{
contract_assert (i >= 0);
return i+1;
}
int main() {
std::cout << '\n';
f(-1);
std::cout << '\n';
}
Wie geht es weiter?
In meinem nächsten Artikel werde ich mich mit den kleineren Features der C++26-Kernsprache befassen. (rme)