Implémenter des conversions de référentiels

Pour comprendre cet article, il est préférable de comprendre au préalable la précieuse technique des traits.

Il arrive fréquemment, dans un programme, qu'il soit nécessaire d'écrire du code de conversion de format, de type ou de valeur. Un cas particulièrement irritant est celui de la conversion de données d'un référentiel à l'autre.

Ce cas est rencontré, par exemple, lorsqu'un programme doit utiliser à la fois plusieurs outils approchant un même problème à l'aide de structures de données semblables mais différentes (trois systèmes d'axes distincts pour une description 3D, par exemple) ou à l'aide de données de formats connexes à une conversion près (pensez à des distances encodées selon le système impérial et à d'autres encodées selon le système métrique).

Lorsque la conversion est simple, comme dans le cas où chaque format est identique à un autre à un facteur multiplicatif constant près, une solution est de conserver une gamme de constantes multiplicatives globales et d'appliquer les multiplications manuellement. Certaines conversions, en contrepartie, sont moins simples (pensons au passage de coordonnées polaires à des coordonnées Euclidiennes ) et il serait souhaitable d'avoir une solution à la fois générale et efficace à tous les problèmes de ce genre.

Démonstration par l'exemple – conversion de température

Un exemple type, fréquemment rencontré dans des livres d'introduction à la programmation, est celui de la conversion de températures.

On peut par exemple envisager une fonction de conversion de °F à °C et une autre de conversion de °C à °F comme suit.

enum { SEUIL_GEL_EAU_FAHRENHEIT = 32 };
double FahrenheitACelsius(double Fahrenheit) noexcept {
   return (Fahrenheit - SEUIL_GEL_EAU_FAHRENHEIT) * 5.0 / 9.0;
}
double CelsiusAFahrenheit(double Celsius) noexcept {
   return SEUIL_GEL_EAU_FAHRENHEIT + 9.0 / 5.0 * Celsius;
}

Le problème se complique si on ajoute d'autres formats dans et vers lesquels convertir.

En effet, en ajoutant la notation en degrés Kelvin, on en arrive à six cas de conversion possibles, avec une implémentation possible sous la forme suivante (je ne répéterai pas les deux premières fonctions par souci d'économie).

Remarquez le passage par un format intermédiaire jouant le rôle de format neutre (ici : la notation en degrés Celsius) pour simplifier le tout et réduire le risque d'erreurs.

enum { ZERO_ABSOLU_CELSIUS = -273 }
double CelsiusAKelvin(double Celsius) noexcept {
   return Celsius - ZERO_ABSOLU_CELSIUS;
}
double KelvinACelsius(double Kelvin) noexcept {
   return Kelvin + ZERO_ABSOLU_CELSIUS;
}
double FahrenheitAKelvin(double Fahrenheit) noexcept {
   return CelsiusAKelvin(FahrenheitACelsius(Fahrenheit));
}
double KelvinAFahrenheit(double Kelvin) noexcept {
   return CelsiusAFahrenheit(KelvinACelsius(Kelvin));
}

C'est une tactique commune en telle situation, et qui ne coûte pas vraiment en temps d'exécution puisque, dans la majorité des cas, les opérations mathématiques sur les constantes impliquées dans les conversions seront résolues ou simplifiées à la compilation.

Notez aussi que j'ai omis les conversions d'un format vers lui-même (de Celsius à Celsius, par exemple) qui sont redondantes mais dont nous devrons nous préoccuper si nous souhaitons en arriver à une approche générale et efficace.

Cette approche, bien que pleinement opérationnelle, entraîne en pratique un certain nombre de problèmes :

  • Le code client doit être exprimé en termes primitifs (on doit penser en termes de valeurs plutôt qu'en termes symboliques)
  • Le code client doit être conscient des conversions à réaliser, donc des fonctions de conversion à invoquer (la représentation primitive étant un nombre à virgule flottante de double précision), ce qui signifie que
  • Le code ne peut automatiser les conversions requises. Du code utilisant une stratégie comme celle-ci est implicitement fragile

Une solution plus OO et un peu plus robuste est de passer par une classe Temperature qui représenterait à l'interne les températures sous une forme neutre (disons en degrés Celsius). Cette classe aurait par exemple des accesseurs et des mutateurs pour des valeurs selon diverses représentations.

Remarquez que Temperature est un simple type valeur et que son constructeur par copie, son affectation et son destructeur par défaut sont tous très convenables.

On pourrait ajouter quelques opérations à Temperature (en particulier les opérateurs relationnels), mais le travail à faire est banal.

enum {
   SEUIL_GEL_EAU_FAHRENHEIT = 32,
   ZERO_ABSOLU_CELSIUS = -273 // approx.
};
class Temperature {
   double valeur{}; // neutre
public:
   Temperature() = default;
   double GetCelsius() const noexcept {
      return valeur;
   }
   double GetKelvin() const noexcept {
      return valeur - ZERO_ABSOLU_CELSIUS;
   }
   double GetFahrenheit() const noexcept {
      return SEUIL_GEL_EAU_FAHRENHEIT + 9.0 / 5.0 * valeur;
   }
   void SetCelsius(double val) noexcept {
      valeur = val;
   }
   void SetKelvin(double val) noexcept {
      valeur = val + ZERO_ABSOLU_CELSIUS;
   }
   void SetFahrenheit(double val) noexcept {
      valeur = (val - SEUIL_GEL_EAU_FAHRENHEIT) * 5.0 / 9.0;
   }
};

Cette solution est acceptable mais demeure manuelle, du fait que l'interface repose strictement sur des types primitifs et que Temperature se trouve chargée d'un nombre arbitrairement grand d'accesseurs et de mutateurs (deux par format supporté).

Il est possible, avec un peu d'imagination, de faire bien mieux.

Représentation efficace des référentiels et des conversions[1]

Pour illustrer ce que nous allons chercher à faire, je commencerai par proposer un programme de test possible.

Sachant que 5 degrés Celsius correspond à (approximativement) 278 degrés Kelvin et à 41 degrés Fahrenheit, nous souhaiterions que le code proposé à droite affiche 5 278 41.

Comprenons que :

  • L'affectation de 5 à c dépose dans c ce qu'il convient de considérer comme 5 Celsius, puis que
  • L'affectation de c à k dépose dans k l'équivalent de c en degrés Kelvin, et que
  • L'affectation de k à f dépose dans f l'équivalent de k en degrés Fahrenheit

Je prends le pari que cette écriture vous semblera naturelle (du moins, je dois vous avouer qu'elle me semble naturelle). Remarquez que l'acte d'affecter une température à une autre implique une conversion lorsque cela s'avère opportun et qu'une température est un type valeur à part entière.

// ...
int main() {
   Temperature<Kelvin> k;
   Temperature<Celsius> c;
   Temperature<Fahrenheit> f;
   f = k = c = 5;
   cout << c << ' '
        << k << ' '
        << f << endl;
}

En gros, notre approche ira comme suit :

Cette approche minimisera le travail à réaliser pour ajouter un type de température au modèle. Notre exemple présentera un système à trois types (Celsius, Kelvin et Fahrenheit).

Les concepts représentant les températures

Les températures seront, tel qu'annoncé, représentées sous forme de purs concepts. Ainsi, les classes Celsius, Fahrenheit et Kelvin seront toutes trois des classes vides, qui ne font qu'exister. Ce sont en fait des catégories, dans un sens semblable aux catégories d'itérateurs.

class Celsius {};
class Fahrenheit {};
class Kelvin {};

Ce sera pour nous à la fois une condition nécessaire et suffisante pour mettre en application les techniques auxquelles nous aurons recours.

Ajouter une sorte de température à notre modèle impliquera donc la nommer à l'aide d'une classe conceptuelle (vide) comme celles-ci.

Les traits des températures

Nous décrirons les caractéristiques d'une température donnée sous la base de traits. Pour nos fins, les traits requis seront les suivants :

  • Définir le type utilisé pour représenter la valeur selon laquelle la température est encodée (type interne et public value_type)
  • Définir une méthode de conversion vers un format neutre (méthode to_neutral()) et une méthode de conversion à partir d'un format neutre (méthode from_neutral())
  • Définir une méthode exposant la valeur du seuil auquel gèle l'eau[2], pour faciliter la construction par défaut; et
  • À titre utilitaire, offrir une méthode permettant de connaître le nom de la température.

Notre convention sera que la représentation neutre de température sera l'expression en degrés Celsius. Notez que le cas général restera indéfini pour restreindre les risques d'erreur à l'exécution.

Avec C++ 11, la fonction temperature_traits<Celsius>::gel_eau() peut avantageusement être qualifiée constexpr, indiquant ainsi que, bien qu'il s'agisse d'une fonction, la valeur qu'elle retourne est une constante connue à la compilation, ce qui permettra de nouvelles (et fort pertinentes, à mon avis) optimisations.

D'ailleurs, cette optimisation sera fréquemment applicable dans les cas de traits comme ceux de std::numeric_limits ou ceux présentés dans cet article.

#include <string_virw>
using namespace std::literals;
struct base_temperature_traits {
   using value_type = long double;
};
template <class T>
   struct temperature_traits;
template <>
   struct temperature_traits<Celsius> : base_temperature_traits {
      static constexpr value_type to_neutral(value_type val) noexcept {
         return val;
      }
      static constexpr value_type from_neutral(value_type val) noexcept {
         return val;
      }
      static constexpr auto nom() noexcept {
         return "Celsius"sv;
      }
      static constexpr value_type gel_eau() noexcept {
         return 0;
      }
   };

Les traits décrivant la représentation d'une température en degrés Kelvin constitueront une spécialisation du trait générique et respecteront les mêmes règles.

Cette approche réduit fortement la complexité intrinsèque à l'ajout de types de températures puisque chaque type de température a un coût descriptif fixe, peu importe le nombre de températures supportées au total.

template <>
   struct temperature_traits<Kelvin> : base_temperature_traits  {
   private:
      static constexpr value_type DELTA_ZERO_ABSOLU = 273;
   public:
      static constexpr value_type to_neutral(value_type val) noexcept {
         return val - DELTA_ZERO_ABSOLU;
      }
      static constexpr value_type from_neutral(value_type val) noexcept {
         return val + DELTA_ZERO_ABSOLU;
      }
      static constexpr auto nom() {
         return "Kelvin"sv;
      }
      static constexpr value_type gel_eau() noexcept {
         return 273;
      }
   };

Sans surprises, les traits pour la température exprimée en degrés Fahrenheit sont aussi simples que ceux pour les températures exprimées en degrés Celsius ou en degrés Kelvin.

template <>
   struct temperature_traits<Fahrenheit> : base_temperature_traits  {
      static constexpr value_type to_neutral(value_type val) noexcept {
         return (val - gel_eau()) * 5.0 / 9.0;
      }
      static constexpr value_type from_neutral(value_type val) noexcept {
         return gel_eau() + 9.0 / 5.0 * val;
      }
      static constexpr auto nom() {
         return "Fahrenheit"sv;
      }
      static constexpr value_type gel_eau() noexcept {
         return 32.0;
      }
   };

La classe générique Temperature

Sans être très complexe, la classe générique Temperature nous demandera un peu de réflexion.

Elle sera générique sur la base d'un type conceptuel de température (par exemple la classe Kelvin) et représentera sa valeur à l'aide du value_type défini par les traits de ce type conceptuel.

#include <utility>
template <class Dest, class Src>
   constexpr typename temperature_traits<Dest>::value_type
      temperature_cast(const typename temperature_traits<Src>::value_type&);
template <class T, class V = typename temperature_traits<T>::value_type>
   class Temperature {
   public:
      using value_type = V;
   private:
      value_type val = temperature_traits<T>::gel_eau();
   public:
      value_type valeur() const {
         return val;
      }

Son constructeur par défaut constituera le seuil du gel de l'eau pour le type de température représenté. Encore une fois, les traits nous seront d'un grand secours ici.

Le constructeur de copie sera implicite, et la précieuse méthode swap() sera banale... Nous pourrions presque l'omettre, si ce n'était du – ici très pertinent – opérateur d'affectation par conversion.

   public:
      Temperature() = default;
      constexpr Temperature(value_type valeur) : val{ valeur } {
      }
      // Sainte-Trinité Ok
      constexpr void swap(Temperature &autre) {
         using std::swap;
         swap(val, autre.val);
      }

Le premier cas subtil apparaît dans le constructeur de conversion, l'une des pièces clés de notre modèle : après tout, nous voulons automatiser un mécanisme de création efficace permettant par exemple la construction d'une température en degrés Kelvin à partir d'une température en degrés Fahrenheit.

À cet effet, examinez attentivement la notation choisie pour réaliser l'implémentation proposée à droite.

      template <class U>
         constexpr Temperature(const Temperature<U> &autre)
            : val{ temperature_cast<T,U>(temp.valeur()) } {
         }

La valeur d'une température de type T sera celle d'un type T suivant une conversion de température du type U au type T, opération nommée ici un temperature_cast<T,U>. Nous verrons un peu plus bas comment cette fonction sera implémentée.

L'opérateur d'affectation, pour un type apparenté devient banal, comme à l'habitude, suite à la définition de swap() et des constructeurs de copie et de conversion.

      template <class U>
         constexpr Temperature& operator=(const Temperature<U> &temp) {
            Temperature{ temp }.swap(*this);
            return *this;
         }

Étant donné la conventionnelle méthode valeur() et présumant un type value_type ayant des propriétés arithmétiques normales pour un nombre, les opérateurs relationnels vont de soi.

Notez que tous ont été exprimés ici en fonction des opérateurs ==, < et de la négation logique. Ceci pourrait nous permettre de simplifier encore la classe Temperature si nous avions recours à des techniques d'injection et d'enchaînement de parents.

Avec C++ 20, écrire operator<=> suffirait ici.

Comparer des nombres à virgule flottante avec == est malsain. Nous aurions pu utiliser une technique plus raffinée et éliminer, potentiellement, bien des heurts : ../Maths/Assez-proches.html

      constexpr bool operator==(const Temperature &autre) const noexcept {
         return valeur() == autre.valeur();
      }
      constexpr bool operator!=(const Temperature &autre) const noexcept {
         return !(*this == temp);
      }
      constexpr bool operator<(const Temperature &autre) const noexcept {
         return valeur() < autre.valeur();
      }
      constexpr bool operator<=(const Temperature &autre) const noexcept {
         return !(autre.valeur() < valeur());
      }
      constexpr bool operator>(const Temperature &autre) const noexcept {
         return autre.valeur() < valeur();
      }
      constexpr bool operator>=(const Temperature &autre) const noexcept {
         return !(valeur() < autre.valeur());
      }
      constexpr Temperature operator-() const noexcept {
         return { -valeur() };
      }
   };

Projeter une Temperature sur un flux

L'écriture d'une Temperature sur un flux va de soi si sa valeur est sérialisable :

template <class T>
   std::ostream& operator<<(std::ostream &os, const Temperature<T> &temp) {
      return os << temp.valeur();
   }

Il pourrait être intéressant d'envisager une projection qui inclurait aussi le nom de l'unité de mesure ou un symbole pour le représenter (p. ex. : F pour Fahrenheit) car cela permettrait dans certains programmes de définir un opérateur d'extraction d'un flux capable de déduire la catégorie de température à consommer. Si vous souhaitez le faire, procédez à partir de temperature_traits<T> et enrichissez ces traits en conséquence.

Conversion générique de températures

L'écriture de l'opération de conversion générique de valeurs de températures est à la fois très complexe et très simple et s'exprime opérationnellement en termes de valeurs et conceptuellement (pour la généricité) en termes de classes conceptuelles.

Pour convertir une température source (type Src) en une température destination (type Dest), nous aurons simplement recours aux traits des deux types impliqués.

template <class Dest, class Src>
   constexpr typename temperature_traits<Dest>::value_type
      temperature_cast(const typename temperature_traits<Src>::value_type &src) {
      return temperature_traits<Dest>::from_neutral(
         temperature_traits<Src>::to_neutral(src)
      );
   }

Le passage d'une valeur dans le modèle source à une valeur dans le modèle de destination se fait en deux temps, soit le passage de la source au modèle neutre puis le passage du modèle neutre à la destination. Les opérations étant simples et génériques, le code généré par le compilateur sera probablement optimal (présumant des traits bien écrits).

Exemple de code client

Un exemple de code client serait celui proposé ici.

int main() {
   using namespace std;
   Temperature<Kelvin> k = 3;
   Temperature<Celsius> c = k;
   cout << c << " "
        << temperature_traits<Celsius>::nom() << endl;
   k = c;
   cout << k << " "
        << temperature_traits<Kelvin>::nom() << endl;
   if (k == c)
      cout << c << " " << temperature_traits<Celsius>::nom()
           << " == "
           << k << " " << temperature_traits<Kelvin>::nom()
           << endl;
   else
      cout << c << " " << temperature_traits<Celsius>::nom()
           << " != "
           << k << " " << temperature_traits<Kelvin>::nom()
           << endl;
   Temperature<Fahrenheit> f = Temperature<Celsius>(5);
   cout << f << " " << temperature_traits<Fahrenheit>::nom() << endl;
}

Remarquez que toutes les conversions sont implicites et efficaces, passant systématiquement par le mécanisme de construction de conversion. Même l'affectation se fait selon ce mode, reposant sur la paire copie et swap().

Une conversion manuelle ne demande rien de plus que la création d'une variable temporaire – et vous conviendrez que cette opération ne coûte, avec ce modèle, pratiquement rien.

Solution plus sophistiquée

Depuis C++ 11, avec l'avènement de littéraux maison et d'expressions constantes généralisées, il est possible de raffiner encore plus notre solution, de manière à la rendre essentiellement optimale...

Voici donc sans plus tarder une solution complète, qui compile sans problème avec la plupart des compilateurs récents au moment d'écrire ceci.

Code client légèrement adapté

Nous débuterons par le code client, légèrement adapté pour illustrer notre propos.

J'ai ajusté le code client de l'exemple précédent pour montrer ce que nous allons viser avec cette version du programme :

  • Notez tout d'abord l'expression auto k = 3_K; qui remplace Temperature<Kelvin> k = 3;. Notez le recours à auto pour choisir le type de la variable k à partir de l'expression qui sert à l'initialiser : ceci signifie que 3_K doit représenter « trois degrés Kelvin », donc que nous devons être en mesure d'écrire nos propres littéraux
  • De la même manière, 0_C représentera « zéro degrés Celsius »
  • Les littéraux qui ne sont pas qualifiés seront des entiers ou des nombres à virgule flottante, comme à l'habitude
  • J'ai intégré l'affichage de l'unité de mesure à l'opérateur de projection sur un flux, pour alléger l'écriture. Nous aurions pu faire ceci dans les exemples précédents aussi
int main() {
   using namespace std;
   auto k = 3_K;
   Temperature<Celsius> c = k;
   cout << c << endl;
   k = c;
   cout << k << endl;
   if (k == c)
      cout << c << " == " << k << endl;
   else
      cout << c << " != " << k << endl;
   c = -40;
   Temperature<Fahrenheit> f = 0_C;
   cout << f << endl;
   f = k = c = 5;
   cout << f << endl;
}

Intégration de assez_proches()

Puisque nous devons ultimement comparer des températures sur la base de leurs valeurs, et puisque ces valeurs sont susceptibles d'être représentées par des nombres à virgule flottante, il nous faut une manière propre de réaliser cette comparaison.

J'ai utilisé ici la technique décrite dans ../Maths/Assez-proches.html, en prenant soin d'avoir recours aux expressions constantes généralisées du fait qu'il s'agit (à mon avis) d'un beau cas d'application.

#include <iostream>
#include <string>
#include <iosfwd>
#include <type_traits>
class exact {};
class flottant {};
// std::abs() vient du langage C et n'est pas constexpr
template <class T> constexp auto absolue(T val) noexcept {
   return val < 0: -val : val;
}
template <class T> constexpr const T seuil_precision = static_cast<T>(0.000001);
template <class T>
   constexpr bool assez_proches(T a, T b, exact) {
      return a == b;
   }
template <class T>
   constexpr bool assez_proches(T a, T b, flottant) {
      return absolue(a - b) < seuil_precision<T>;
   }
template <class T>
   constexpr bool assez_proches(T a, T b) {
      return assez_proches(
         a, b, std::conditional_t<
            std::is_floating_point_v<T>, flottant, exact
         >{}
      );
   }

Ajustement de la classe Temperature<T,V>

La classe Temperature<T,V> a évidemment besoin elle aussi de quelques retouches.

Tout d'abord, il importe de déclarer le prototype de temperature_cast.

// temperature_traits (voir plus haut)
template <class Dest, class Src>
   constexpr typename temperature_traits<Dest>::value_type temperature_cast
      (const typename temperature_traits<Src>:: value_type &);

Presque tous les services de la classe Temperature<T,V> sont constexpr.

Cela signifie qu'il est, en pratique, possible d'utiliser des Temperature<T> pour différents types de températures, et d'obtenir littéralement (!) les conversions et autres opérations à coût zéro.

template <class T, class V = typename temperature_traits<T>::value_type>
   class Temperature {
   public:
      using value_type = V;
   private:
      value_type val = temperature_traits<T>::gel_eau();
   public:
      Temperature() = default;
      constexpr Temperature(value_type valeur) : val{ valeur } {
      }
      template <class U>
         constexpr Temperature(const Temperature<U> &autre) : val{ temperature_cast<T,U>(temp.valeur()) } {
         }
      constexpr void swap(Temperature &autre) {
         using std::swap;
         swap(val, autre.val);
      }
      template <class U>
         constext Temperature& operator=(const Temperature<U> &autre) {
            Temperature{ autre }.swap(*this);
            return *this;
         }
      constexpr value_type valeur() const { return val; }
      constexpr auto nom() const { return temperature_traits<T>::nom(); }
      // avec C++20, les six opérateurs relationnels se limitent à operator<=>
      constexpr bool operator==(const Temperature &autre) const {
         return assez_proches(valeur(), autre.valeur());
      }
      constexpr bool operator!= (const Temperature &autre) const {
         return !(*this == autre);
      }
      constexpr bool operator< (const Temperature &autre) const {
         return valeur() < autre.valeur();
      }
      constexpr bool operator<=(const Temperature &autre) const {
         return !(autre.valeur() < valeur());
      }
      constexpr bool operator>(const Temperature &autre) const {
         return autre.valeur() < valeur();
      }
      constexpr bool operator>=(const Temperature &autre) const {
         return !(valeur() < autre.valeur());
      }
      constexpr Temperature operator-() const {
         return { -valeur() };
      }
   };

Les opérateurs permettant d'exprimer des Temperature<T> sous la forme de littéraux maison, addition au code pour cette version, sont proposés à droite.

Remarquez que j'accepte les nombres à virgule flottante et les entiers, donc des expressions comme 0.5_C (tout juste au-dessus du seuil de gel de l'eau) ou 50_F (un peu plus chaud). Dans le cas où un entier est utilisé pour le littéral, je dois appeler le constructeur avec parenthèses, car les accolades sont plus strictes et rejetteraient la conversion implicite d'un unsigned long long à un long double.

Dans tous les cas, nos littéraux sont constexpr car ils appellent des constructeurs constexpr.

constexpr Temperature<Celsius> operator "" _C(long double val) {
   return { val };
}
constexpr Temperature<Celsius> operator "" _C(unsigned long long val) {
   return { static_cast<Temperature<Celsius>::value_type>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(long double val) {
   return { val };
}
constexpr Temperature<Fahrenheit> operator "" _F(unsigned long long val) {
   return { static_cast<Temperature<Fahrenheit>::value_type>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(long double val) {
   return { val };
}
constexpr Temperature<Kelvin> operator "" _K(unsigned long long val) {
   return { static_cast<Temperature<Kelvin>::value_type>(val) };
}

Enfin, outre le fait que j'ai intégré l'unité de mesure dans l'affichage d'une température, le reste n'a pas vraiment changé.

template <class T>
   std::ostream& operator<<(std::ostream &os, const Temperature<T> &temp) {
      return os << temp.valeur() << ' ' << temp.nom();
   }

template <class Dest, class Src>
   constexpr typename temperature_traits<Dest>::value_type temperature_cast
      (const typename temperature_traits<Src>::value_type &src) {
      return temperature_traits<Dest>::from_neutral(
         temperature_traits<Src>::to_neutral(src)
      );
   }

Nous avons obtenu, au passage, une amélioration significative du code généré, et un gain d'expressivité. Pas si mal!

Lectures complémentaires

Quelques liens pour enrichir le propos.


[1] Merci à Vincent Echelard et à François Jean : l'idée de cette stratégie m'est venue en bavardant avec ces deux illustres et forts pertinents collègues.

[2] Une simple constante pourrait ne pas suffire du fait que le type temperature_traits<T>::value_type pourrait ne pas être entier. Cela dit, ce problème aura des conséquences bien moindres avec l'avènement d'expressions constantes généralisées.


Valid XHTML 1.0 Transitional

CSS Valide !