Assertions statiques

Pour en savoir plus sur les assertions dynamiques, voir ../Developpement/Gestion-erreurs.html#assertion_dynamique

Les langages C et C++ (entre autres) offrent depuis longtemps une bibliothèque dite d'assertions. Une assertion dynamique (fonction ou macro assert()) évalue une condition et fait en sorte que le programme plante si cette condition est fausse.

L'idée des assertions est de faire planter un programme avant qu'il ne soit trop tard et de faciliter le débogage à partir de points d'échec reconnaissables pour les programmeuses et les programmeurs[1].

Vouloir planter? Souhaiter ne pas compiler?

Il peut paraître étrange qu'on discute ici (et en bien!) de programmes qui ne compilent pas ou qui plantent à l'exécution. Il faut comprendre que, comme dans le cas de certaines catégories d'exceptions, il est parfois préférable de ne pas récupérer d'une erreur et de planter pour éviter que l'erreur en question passe inaperçue et entraîne des problèmes plus graves.

De même, lorsque cela s'avère possible, il est nettement préférable de détecter les problèmes dès la compilation d'un programme que lors de son exécution. L'économie en temps et en effort est rien de moins que considérable.

À titre historique, voir Incompilable.html, mais sachez que static_assert, décrit ici, est un mécanisme bien plus chouette.

Le « problème » des assertions est qu'elles sont dynamiques, au sens où le programme doit s'exécuter et rencontrer la condition pour provoquer un signalement du problème. Pourtant, certaines conditions devraient être reconnaissables dès la compilation.

Un avantage conceptuel des assertions est qu'elles offrent une forme d'autodocumentation des programmes.

En effet, documentant à même le programme les conditions qui doivent être rencontrées pour que son fonctionnement soit correct, elles clarifient et balisent les intentions des programmeuses et des programmeurs. L'idiome NVI est particulièrement utile pour implémenter, par des assertions ou autrement, le respect des invariants d'une classe.

Pensons par exemple à un programme devant être compilé sur plusieurs plateformes et où, pour des raisons techniques qui lui sont propres, il est important qu'un short soit plus petit qu'un int (donc que sizeof(short)<sizeof(int)), ce qui n'est pas garanti par le standard de C++ (les types short et int peuvent être de même taille et le sont parfois sur des machines à « haute performance »).

Ce type de contrainte est commun dans du code de bibliothèque (STL, par exemple, ou un moteur de jeu commercial), et apparaît plus souvent qu'on ne le pense dans des types génériques ou dans des sous-programmes génériques.

Il serait possible d'insérer l'instruction assert(sizeof(short)<sizeof(int)); à des points stratégiques dans un tel programme, mais cela comporte le double désavantage de ralentir son exécution (le test, bien que bref, devant être réalisé) et de demander l'exécution du programme alors que la détection du problème aurait pu être réalisée dès la compilation (car sizeof est un opérateur du langage dont l'évaluation est faite à la compilation).

On souhaiterait donc, dans un tel cas, une assertion statique, capable d'évaluer une condition connue à la compilation et d'empêcher la compilation du programme si cette condition ne s'avère pas.

Assertions statiques avec C++ 11

Puisque les assertions statiques sont extrêmement utiles en pratique, elles ont été intégrées à C++ 11 de manière pleine et entière. Sachant cela, étant donné une condition statique devant s'avérer pour que votre programme soit correct, vous pouvez écrire ceci :

static_assert(condition_statique, "message d'erreur");

...ce qui est à la fois direct, flexible et gentil. Étant supporté à même le langage, le compilateur travaille de concert avec vous, et les assertions statiques peuvent être utilisées partout dans un programme.

Exemple d'utilisation : imaginez que vous ayez rédigé un programme de manière quelque peu imprudente, en utilisant le type char comme un entier signé, pour constater une fois une part importante du code source rédigé que le standard ne garantit pas que char soit signé (le caractère signé ou non du type char dépend du compilateur). Vous savez alors que votre programme serait incorrect (ou dangereux!) s'il était généré avec un compilateur pour lequel char est non-signé; dans ce cas, mieux vaut s'assurer qu'il ne compilera pas!

Le truc : peut-on définir une condition statique pour valider que char soit signé? Bien sûr : il suffit de placer -1 dans un char et de vérifier que le résultat soit signé. Il est donc possible de se prémunir contre les compilateurs pour lesquels notre supposition ne s'avérerait pas comme suit :

static_assert(static_cast<char>(-1)<0, "Le type char doit etre signe");

Assertions statiques avec C++ 03

Cette section montre comment il était possible de réaliser une assertion statique avec C++ 03; cependant, si votre compilateur supporte C++ 11, alors préférez les outils contemporains qui sont bien meilleurs!

Il existe plusieurs techniques pour définir une assertion statique avec C++ 03. En voici une qui est à la fois simple, élégante et très efficace.

Tout d'abord, l'assertion repose sur un booléen (l'évaluation de la condition). Il importe donc de définir un template applicable à un bool. Dans cet exemple, le template en question se nommera static_assert.

Le template sera déclaré pour tout bool mais ne sera spécialisé (vide!) que pour la valeur booléenne true (et surtout pas pour la valeur false).

Le résultat de cette stratégie est que si un programme exprime un static_assert pour un booléen faux, le compilateur cherchera à instancier ce type pour lequel aucune définition n'existe, donc le programme ne pourra être compilé.

template <bool>
   struct static_assert;
template <>
   struct static_assert<true>
   {
   };

Par exemple, ceci ne compilera pas si l'assertion mentionnée plus haut vérifiée sur une plateforme donnée, ce qui permettra de diagnostiquer le problème avant même la période de tests.

Cette technique a pour grand avantage de provoquer des erreurs de compilation aux endroits où les problèmes sont détectés, et permet aux compilateurs de pointer le doigt vers la ligne où apparaît l'erreur en question. Conséquemment, il est d'usage de donner à la variable un nom descriptif de l'erreur relevée. Cependant, deux situations peuvent se produire :

  • Soit l'assertion statique n'est pas respectée, dans quel cas le programme ne compile pas et la variable au type incomplet décrit la nature du problème
  • Soit l'assertion statique est respectée, dans quel cas le programme compile mais produit, sur la majorité des compilateurs aujourd'hui, un avertissement à l'effet que la variable résultante n'est pas utilisée dans le programme – et le compilateur a raison! Mais nous le savons, et nous le souhaitons (c'est la raison d'être de la technique, après tout)
int main()
{
   static_assert <
      (sizeof(short) < sizeof(int))
   > short_doit_etre_plus_petit_que_int;
   // ...code dépendant de cette assertion...
}

Une solution pour éliminer l'avertissement est d'avoir recours à des #pragma, donc à des directives non portables du compilateur que nous utilisons. Cette solution fonctionne mais est agaçante du fait qu'elle implique de faire du cas par cas dans notre code source, pour fins de portabilité.

Herb Sutter a proposé une autre solution, visible à droite. La fonction générique unused() prend en paramètre une référence sur quelque chose, ne nomme pas ce paramètre (pour éviter que le compilateur ne se plaigne d'une variable inutilisée) et... ne fait rien.

Ne reste plus qu'à passer la variable que constitue l'assertion statique en paramètre à unused(). Ce faisant, le compilateur estime que la variable est utilisée par le sous-programme l'ayant définie, et peut optimiser cette fausse utilisation sans peine. Une solution pleinement portable et sans coûts.

template <class T>
   void unused(const T &)
      { }
int main ()
{
   static_assert <
      (sizeof(short) < sizeof(int))
   > short_doit_etre_plus_petit_que_int;
   unused(short_doit_etre_plus_petit_que_int);
   // programme dépendant de cette assertion
}

Une tactique éprouvée pour générer des assertions statiques pré-C++ 11 était de définir un tableau de char de taille 0 dans le cas où la condition de l'assertion n'est pas rencontrée, ce qui génère effectivement une erreur à la compilation. Cela fonctionne en pratique, mais n'offre pas l'élégance de la technique proposée ici.

Lectures complémentaires

Quelques liens pour enrichir le propos.


[1] Certaines versions des assertions cessent d'être opérationnelles lorsque le programme est généré en mode de production (en mode Release). L'avantage est que le programme, dans sa version commerciale, n'est plus ralenti par ces tests. Le désavantage, évidemment, est le risque accru de problèmes sérieux et difficiles à dépister. Pour une histoire d'horreur à ce sujet, voir cet article (l'entrée du 23 octobre 2002) suggéré Dominik Bauset, de la cohorte 03 du DDJV.


Valid XHTML 1.0 Transitional

CSS Valide !