C++20 – Les contrats

Bonjour à tous !

Aujourd’hui, un article au sujet d’une fonctionnalité de C++20 qui va beaucoup apporter au langage : les contrats. Trois nouveaux attributs ont de fait été ajoutés : [[expects]], [[ensures]] et [[assert]].

A l’issu du meeting C++ de l’ISO à Rapperswil en Suisse le 9 juin, la p0542 – proposant un mécanisme de programmation par contrat en C++ – a été acceptée pour C++20.

 Les contrats

Lorsqu’on parle d’un logiciel, on va différencier deux principes généraux : la robustesse (robustness) et la justesse (correctness).

La robustesse d’un programme va qualifier sa resistance aux situations inattendues et exceptionnelles, celles qui ne sont pas de son fait. Le mécanisme d’exception du C++ permet de réagir face à ces situations exceptionnelles (problème réseau ou manque de mémoire par exemple). L’exception est traitée en amont soit pour récupérer de l’erreur (si c’est possible), soit pour quitter proprement le programme.

La justesse du programme définit le fait que le programme soit correct dans sa logique interne, c’est à dire qu’il est absent d’erreur de programmation.

Pour assurer les invariants (des classes ou du programme par exemple) et ainsi s’assurer de la justesse, on a à disposition :

  • les préconditions : ce qui correspond aux prédicats sur les paramètres de la fonction.
  • les postconditions : les prédicats que la fonction doit vérifier à son retour.
  • les assertions : qui correspondent à des prédicats vérifiés localement dans le corps de la fonction.

Sans rentrer du débat programmation défensive / programmation par contrat, on considère d’une manière générale que les exceptions doivent être limitées aux situations exceptionnelles (une proposition d’Herb Sutter vise d’ailleurs actuellement à trouver un moyen de ne plus lever std::bad_alloc lorsqu’on est en manque de mémoire). Une erreur de programmation ne devrait pas être vérifiée à l’aide du mécanisme d’exception mais plutôt en vérifiant les préconditions et les postconditions de la fonction.

Nous allons donc voir les trois attributs [[assert]], [[expects]] et [[ensures]], représentant respectivement l’assertion, la précondition et la postcondition. Il s’agit des nouveaux attributs destinés à apporter un support de la programmation par contrat en C++. Ceux-ci obéissent à des règles de syntaxes différentes des autres attributs déjà sortis depuis C++11.

assert

Les assertions sont gérée en C++20 par l’attribut [[assert]]. Cet attribut permet de déclarer une assertion, il peut être vu comme une version moderne de la macro C assert().

Cet attributs est assez particulier puisqu’il s’utilise à la manière d’une instruction et est suivis par un point-virgule :

expects et ensures

[[expects]] et [[ensures]] forment les contracts conditions et permettent d’exprimer respectivement une précondition et une postcondition. Ils s’appliquent à une fonction de cette manière :

Les deux attributs attendent une expression booléenne. Les arguments de la fonction et toutes les variables globales ( constexpr, static etc…) peuvent être utilisés. Dans le cas de  [[ensures]], il est possible d’introduire l’identifiant qui sera utilisé comme retour de fonction de manière à l’utiliser dans la postcondition :

Niveau d’assertion

Pour les trois attributs, il est possible de spécifier un niveau d’assertion (assertion level ou contract level) qui permet de définir dans quelle mesure le contrat sera vérifié à l’exécution : default, audit et axiom.

Le niveau default est implicite si rien n’est indiqué et correspond au cas où le coût de la vérification du contrat est faible par rapport à l’exécution de la fonction :

Le niveau audit correspond aux vérifications qui ont un coût non-négligeable :

Le niveau axiom indique que le contrat ne sera jamais vérifié à l’exécution. L’expression doit cependant être du C++ valide (notez qu’elle peut contenir des appels à des fonctions non définies puisqu’elle n’est pas évaluée), l’expression sert d’indications aux outils d’analyse, l’optimiseur et les développeurs qui liront le code :

audit et axiom ne sont pas des mots clefs, mais des identificateurs avec une sémantique particulière (identifiers with special meaning) dans certains contextes (tels que override ou final).

Build level et violation de contrat

Pour chaque unité de traduction, il est possible de spécifier un build-level :

  • off : Aucune vérification à l’exécution n’est faite.
  • default (effectué par défaut si rien n’est indiqué): seules les vérifications des contrats avec un niveau d’assertion  default sont effectuées.
  • audit : seules les vérifications de contrats avec un niveau d’assertion default ou audit sont effectuées.

L’implémentation définie la manière dont le build-level est spécifié (il s’agira certainement d’une options du compilateur). De la même manière, il est possible de spécifier le violation continuation mode :

  • off (par défaut) : après l’exécution du violation handler, std::terminate est appelé.
  • on : après l’exécution du violation handler, le programme reprend son exécution.

Le violation handler est une fonction ayant pour signature  void (const std::contract_violation&) pouvant être spécifiée noexcept. Celle-ci est également spécifiée au compilateur de manière implementation-defined et est appelée lorsqu’un contrat est violé. Le type std::contract_violation est une interface standard pour représenter une violation de contrat, sa définition est comme suit :

La spécification des valeurs associées en fonction du contrat violé est disponibles sur la page cppreference correspondante.

Il déconseillé aux implémentation de fournir un moyen de récupérer ou modifier programmatiquement le build-level, le violation_handler ou le violation continuation mode.

Les précisions et les limitations

Il y a quelques règles à connaitre concernant les contract conditions :

  • Deux contract conditions sont considérées comme identiques si le prédicat et le niveau d’assertionsont identiques.
  • Lors de la première déclaration de la fonction, toutes les précondition et postcondition doivent être spécifiées.
  • Lors d’une redéclaration d’une fonction, soit aucune contract condition ne doit être spécifiée, soit toutes : déclarées dans le même ordre, avec le même niveau d’assertion et les même prédicats (les noms de paramètres et du type de retour prédéclaré pouvant changer d’une déclaration à l’autre). les variables .
  • Les contract conditions doivent également être identique d’une unité de traduction à l’autre.
  • L’amitié n’est pas prit en compte dans les contract conditions. Les membres auxquels ont peut accéder dépendent du niveau de visibilité de la fonction :
    • private : private, protected et  public.
    • protected : protected et public.
    • public : uniquement  public.
  • Si une variables est odr-used dans une postcondition, et que celle-ci est modifiée dans le corps de la fonction, il s’agit d’un undefined-behavior :

Sachez qu’à l’heure où j’écris ces lignes, les compilateurs majeurs (gcc, clang, msvc …) n’implémentent pas encore les contrats. Les informations présentées ici n’ont donc pas pu être testées et sont directement issues de la proposition.

Il est cependant possible de simuler en partie les contrats à l’aide de la bonne vielle macro  assert et des fonctions Expects et Ensures de la C++ Core Guidelines Support Library (GSL). Voici un exemple :

Bien entendu, la GSL ne propose pas d’équivalent pour les niveaux d’assertion et le build-level.

Ce nouveau système de contrat basés sur les attributs est donc un grand pas en avant. Cela va permettre un meilleur contrôle des erreurs de programmation grâce à un support venant du langage. Ces informations pourront également profiter aux outils d’analyse statique et à l’optimiseur, ce qui est un plus.

Pour résumer nous avons à notre disposition :

  • Les preconditions avec expects.
  • Les postcondition avec ensures.
  • Un nouveau système d’assertion qui a l’avantage de ne pas être une macro contrairement à   assert() ( [[assert: c == std::complex<float>{0, 0}]]  compile contrairement à assert(c == std::complex<float>{0,0}))) et qui intégré au système de contrat (profitant des niveau d’assertions etc.).
  • Trois niveaux d’assertion ( default, audit, axiom) et trois niveaux de build ( off, default, audit).
  • La possibilité de spécifier le violation handler.
  • Deux modes de recouvrement d’erreur ( off et on).

Il n’y a plus qu’à attendre le support des compilateurs ! En attendant vous pouvez d’ores et déjà utiliser ce qui est proposé par la GSL. Merci d’avoir lu cet article et à bientôt !

Références et ressources

%d bloggers like this: