Conversion et détection de pertes d'information – un narrow_cast

L'idée de la technique proposée ici n'est pas de moi mais bien de Bjarne Stroustrup. Je suis tombé dessus en lisant son sympathique livre Programming principles and practice using C++.

Dans ce qui suit, j'utilise à l'occasion std::numeric_limits, un trait défini pour tous les types primitifs numériques du langage C++ (et extensible à d'autres types, au besoin) que vous trouverez dans l'en-tête standard <limits>. J'omettrai le préfixe std:: pour alléger l'écriture.

Certaines opérations sont fondamentalement à risque de résulter en une perte de précision. Un exemple simple est proposé à droite, où l'affectation de s à i est sans risque mais l'affectation de i à s peut poser problème. De même, l'affectation de i à ui peut mener à une perte de précision, et il en va de même pour l'affectation de ui à i.

La raison pour laquelle ces risques existent (ou non) a trait aux domaines de ces variables :

  • La variable s est de type short, type entier signé dont les bornes vont inclusivement de numeric_limits<short>::min() à numeric_limits<short>::max()
  • La variable i est de type int, type entier signé dont les bornes vont inclusivement de numeric_limits<int>::min() à numeric_limits<int>::max()
  • La variable ui est de type unsigned int, type entier non-signé dont les bornes vont inclusivement de numeric_limits<unsigned int>::min() (c'est-à dire 0UL) à numeric_limits<unsigned int>::max()

La norme du langage C++ ne fixe pas les tailles (ou les bornes) pour ces types, mais elle fixe toutefois certaines relations :

  • sizeof(short)<=sizeof(int). Sachant cela, et sachant que ces deux types entiers sont signés, nous savons que tout short valide peut être représenté par un short valide, même sur un compilateur où il adviendrait que sizeof(short)==sizeof(int). Le compilateur agit donc de manière pleinement légitime en acceptant qu'on puisse affecter un short à un int,, cette opération étant nécessairement sans danger
  • sizeof(int) peut toutefois être supérieur à sizeof(short), et c'est d'ailleurs souvent le cas sur les plateformes contemporaines. Dans ce cas, il existe un grand nombre de int qui ne sont pas des short valides, et l'affectation d'un int à un short peut mener à une perte d'information. Cependant, pour des raisons historiques (et pragmatiques), les compilateurs C++ acceptent sans avertissement qu'on affecte un int à un short, et cette opération est souvent banale (déposer 3 dans un short est sans danger, après tout, et un littéral comme 3 est de type int)
  • Enfin, bien que sizeof(int)==sizeof(unsigned int), il se trouve que la plage de validité d'un int est à moitié positive et à moitié négative, alors que la plage de validité d'un unsigned int est pleinement positive. Ainsi, supposant sizeof(int)==4, la valeur 0xffffffff déposée dans un int représentera alors que la même valeur déposée dans un unsigned int représentera
#include <iostream>
int main() {
   using std::cin;
   short s;
   int i;
   unsigned int ui;
   cin >> s;
   i = s;  // sans risque
   cin >> i;
   s = i;  // problème si i > std::numeric_limits<short>::max()
   ui = i; // problème si i < 0
   cin >> ui;
   i = ui; // problème si ui > std::numeric_limits<int>::max()
}

Valider une affectation d'une donnée d'un type à un autre n'est donc pas nécessairement une question de types. C'est d'abord une question de valeur.

L'exemple proposé à droite met ceci en relief :

  • L'initialisation de s0 est correcte par définition, se basant sur la valeur maximale du domaine de s0
  • L'intialisation de i0 est correcte car s0 et i0 sont tous deux signés et car sizeof(i0)>=sizeof(s0)
  • L'auto-incrémentation de i0 est correcte si sizeof(i0)>sizeof(s0). Ceci peut se vérifier à la compilation par une assertion statique du fait que le raisonnement se base sur les types, pas sur les valeurs
  • Cela dit, si l'auto-incrémentation de i0 est correcte, alors la valeur de i0 dépasse par la suite la borne maximale de l'intervalle de validité de s0, et l'affectation de i0 à s0 provoque alors un débordement
#include <limits>
int main() {
   using std::numeric_limits;
   short s0 = numeric_limits<short>::max(); // Ok
   int i0 = numeric_limits<short>::max(); // Ok
   ++i0; // Ok ssi sizeof(int)>sizeof(short)
   s0 = i0; // débordement!
 }

Un tel débordement est sournois, car il est le plus souvent silencieux. Il arrive même que des programmeurs profitent de cette propriété pour faire des optimisations (je suis coupable de certaines d'entre elles!) alors il n'est pas clair que nous souhaitions les pénaliser systématiquement. Cependant, dans le cas où un débordement poserait problème, il serait bien de pouvoir le détecter et, le cas échéant, lever une exception.

Dans son livre Programming principles and practice using C++, Bjarne Stroustrup propose une technique toute simple pour obtenir précisément ce comportement. Cette technique se décline sous la forme d'une opération de transtypage maison (un Cast) qu'il nomme le narrow_cast, du terme anglais narrowing. Le principe est simple :

  • Le code client cherche à convertir une donnée d'un type A (pour argument type) à un type R (pour result type)
  • La fonction crée d'abord un R à partir d'un A, utilisant la mécanique de construction paramétrique classique, et nomme r ce nouvel objet
  • Ensuite, la fonction crée un A à partir de r, et compare ce nouvel A avec le A original. Si les deux sont différents, alors une perte d'information est survenue et une exception est levée
  • Enfin, si tout s'est déroulé sans heurt, r est retourné
class PerteInformation {};
template<class R, class A>
   R narrow_cast(const A &a) {
      R r(a);
      if (A(r) != a) throw PerteInformation{};
      return r;
   }

Reprenons l'exemple plus haut :

  • Les initialisations de s0 et de i0 sont correctes par définition. Nous n'aurons pas recours à narrow_cast ici puisque ce serait superflu
  • L'auto-incrémentation de i0 est correcte et vérifiée par une assertion statique
  • Enfin, le narrow_cast<short>() appliqué à i0 valide que la conversion de i0 en short à ce stade, étant donné la valeur de i0, entraînerait une perte d'information

Mission accomplie!

// ... inclusions et using...
int main() {
   short s0 = numeric_limits<short>::max(); // Ok
   int i0 = numeric_limits<short>::max(); // Ok
   static_assert(sizeof(short)<sizeof(int), "Oh oh...");
   ++i0; // ok dû au static_assert
   try {
      s0 = narrow_cast<short>(i0); // débordement?
   } catch(PerteInformation&) {
      cerr << "Déposer " << i0
           << " dans un short entraînerait un débordement"
           << endl;
   }
 }

Simple et, dans les circonstances, plutôt efficace.


Valid XHTML 1.0 Transitional

CSS Valide !