Une petite classe rationnel

Le code ci-dessous utilise une technique inspirée directement du Truc Barton-Nackman et de l'idiome CRTP, alors si vous n'êtes pas familières ou familiers avec ces manoeuvres, il est possible que vous souhaitiez leur jeter un coup d'oeil au préalable.

Les équations sur cette page sont présentées avec l'aide de MathJax.

Pour un exemple semblable en C#, voir ceci.

Le standard C++ 11 offre, dans l'en-tête standard <ratio>, un type rationnel statique très bien fait et pouvant servir entre autres à représenter des unités de mesure.

Cependant, il arrive à l'occasion qu'un rationnel dont le numérateur et le dénominateur sont connus à l'exécution soit pertinent, et ça m'est justement arrivé dernièrement – un truc sur la comparaison de deux séquences composées pour voir à quel point elles se ressemblent, mais pour lesquelles l'imprécision inhérente aux nombres à virgule flottante était inacceptable. Un tel type est esquissé entre autres par Scott Meyers dans son livre bien connu Effective C++ (que vous devriez lire si ce n'est déjà fait) et constitue un exercice de programmation amusant.

Si vous avez toutefois besoin d'un tel type et n'avez pas le temps de le coder vous-mêmes, vous serez peut-être intéressé(e) à ce qui suit. Je vous l'offre sans garanties (je l'ai testé avec une rigueur discutable; le code de test apparaît plus bas) mais ça fonctionne bien en pratique pour les cas que j'ai eu besoin de valider.

Deux versions d'un type rationnel suivent, soit une « classique » et une qui est pleinement constexpr (outre pour les entrées / sorties, bien entendu). La seconde est probablement à privilégier dans la majorité des cas.

Implémentation « classique »

Tout d'abord, un choix de design : j'aurais pu faire de rationnel une classe générique sur la base d'un type d'entier particulier (rationnel<int> par exemple), ce qui aurait permis d'en faire une classe entièrement localisée dans un seul fichier (rationnel.h), mais j'ai choisi d'en faire un type concret.

Ceci signifie que certains de ses services sont soit des fonctions inline, soit carrément définis dans un fichier source à part (rationnel.cpp) pour éviter des violations de la règle ODR.

L'en-tête relation.h définit les types relation::equivalence et relation::ordonnancement, qui permettent de générer certains opérateurs à l'aide de l'idiome CRTP :

#ifndef RELATION_H
#define RELATION_H
namespace relation {
   template <class T>
      struct ordonnancement {
         friend bool operator>(const T &a, const T &b)
            { return b < a; }
         friend bool operator<=(const T &a, const T &b)
            { return !(b < a); }
         friend bool operator>=(const T &a, const T &b)
            { return !(a < b); }
      };
   template <class T>
      struct equivalence {
         friend bool operator!=(const T &a, const T &b)
            { return !(a == b); }
      };
}
#endif
#ifndef RATIONNEL_H
#define RATIONNEL_H
#include "relation.h"
#include <type_traits>
#include <cmath>
#include <limits>
#include <functional>
#include <iosfwd>

Pour le design de ma classe, je me suis basé sur la classe std::ratio. Ainsi, nous assurerons en tout temps le respect d'au moins deux invariants, soit :

Pour simplifier des fractions, il est d'usage d'avoir recours au calcul du plus grand commun diviseur (PGCD), tout comme il est pertinent d'avoir recours au calcul du plus petit commun multiple (PPCM) pour déterminer un dénominateur commun pour certains calculs.

Notez que pour le PGCD, j'ai implémenté une λ interne réalisant le calcul (récursif) en tant que tel. Ceci permet de ne pas répéter la validation des intrants.

Pourquoi cette validation? Il se trouve que selon plusieurs écoles de pensée, PGCD(0,0) est indéfini...

Notez au passage qu'une proposition est sur la table pour C++ à l'effet d'ajouter std::gcd() et std::lcm() aux outils standards, et de les offrir en format constexpr. Quand ce changement aura pris effet, nous utiliserons bien entendu ces nouvelles fonctions qui seront sans doute supérieures à ce que nous avons écrit ici.

Enfin, absolute_value() est une version constexpr de std::abs().

class division_par_zero {};
template <class T>
   constexpr T raw_pgcd(T a, T b)
   {
      return b ? raw_pgcd(b, a % b) : a;
   }
template <class T>
   constexpr T pgcd(T a, T b)
   {
      return !a && !b? throw division_par_zero{} : raw_pgcd(a,b);
   }
template <class T>
   constexpr T ppcm(T a, T b)
      { return a * b / pgcd(a,b); }
template <class T>
   constexpr T absolute_value(T val)
      { return val < 0? -val : val; }

La classe rationnel en tant que telle contiendra un numérateur et un dénominateur, tous deux entiers. Remarquez au passage l'application du truc Barton-Nackman.

Notez que j'ai choisi de définir SEP de manière à ce qu'il s'agisse d'un membre de classe servant de séparateur pour un rationnel sérialisé sur un flux. Ce choix est discutable, cependant; procéder à l'aide de traits, pour en arriver à un plus faible couplage entre la représentation d'un rationnel et sa représentation sérialisée, aurait sans doute été préférable.

La méthode privée pick_num_sign() sert à la construction pour déterminer le signe du numérateur. J'ai mis à jour ce passage en 2016 suivant un bogue dans l'écriture un peu trop naïve de ce passage (merci à mon ami Victor Ponce du signalement!)

class rationnel
   : relation::equivalence<rationnel>,
     relation::ordonnancement<rationnel>
{
public:
   using value_type = int;
   static constexpr char SEP = '/';
private:
   value_type num_, denom_;
   static constexpr value_type pick_num_sign(value_type num, value_type denom)
   {
      return num < 0 == denom < 0?
         absolute_value(num) : -absolute_value(num);
   }
public:

Outre le constructeur de copie, implicite, j'ai choisi d'implémenter deux constructeurs paramétriques :

  • Un qui ne prend que le numérateur. Dans ce cas, le code est simple, et tout entier, positif ou négatif, est acceptable. Le dénominateur est alors implicitement . Notez que par souci de conformité avec les autres types, un rationnel par défaut a pour valeur le zéro de son type, soit , et
  • Un autre qui prend un numérateur et un dénominateur, deux entiers. Ici, certains calculs sont requis, soit :
    • éviter les divisions par zéro
    • assurer la présence de signe sur le seul numérateur
    • simplifier la fraction utilisée pour représenter le rationnel. et
    • faire en sorte que, si le numérateur est nul, le dénominateur soit (pour éviter des cas un peu absurdes où )

J'ai rendu impossible la construction d'une rationnel sur la base d'un nombre à virgule flottante, suivant une recommandation intéressante d'Andrzej Krzemieński formulée dans http://akrzemi1.wordpress.com/2014/07/25/inadvertent-conversions/

   constexpr rationnel(value_type num = {})
      : num_{num}, denom_{1}
   {
   }
   rationnel(float) = delete;
   rationnel(double) = delete;
   rationnel(long double) = delete;
   rationnel(value_type num, value_type denom)
      : num_{pick_num_sign(num, denom)},
        denom_{denom? maths_ext::absolute_value(denom) : throw division_par_zero{}}
   {
      if (num_)
      {
         auto d = pgcd(num_, denom_);
         if (d != 1)
         {
            num_ /= d;
            denom_ /= d;
         }
         num_ = pick_num_sign(num_, denom_);
      }
      else
         denom_ = 1;
   }

Comme la plupart des types, mon rationnel exposera des accesseurs publics. Ici, les plus simples seront ceux par lesquels on pourra consulter le numérateur et le dénominateur d'une instance de ce type.

Notez que, du fait que nous devons assurer le maintien de nos invariants, il importe de ne pas exposer les attributs d'une instance de rationnel de manière publique.

   constexpr value_type num() const noexcept
      { return num_; }
   constexpr value_type denom() const noexcept
      { return denom_; }

Une opération simple à implémenter est celle permettant d'obtenir la négation arithmétiqued'un rationnel – l'opérateur - unaire.

En effet, il s'agit simplement ici de générer un rationnel dont le signe est l'inverse de celui du rationnel original.

   constexpr rationnel operator+() const noexcept
      { return *this; }
   rationnel operator-() const noexcept
      { return rationnel{-num(), denom()}; }

Il est intéressant de remarquer que la négation arithmétique pourrait être optimisée de manière importante ici. En effet, nous utilisons un constructeur public, celui à deux paramètres, qui est accessible à tous et responsable, par le fait même, de valider ses intrants, simplifier la fraction, etc. Ici, tous ces calculs sont superflus car la fraction d'origine, *this, est déjà correcte et simplifiée.

Plusieurs options sont possibles pour accélérer cette méthode. À votre avis, quelle serait la meilleure manière de procéder?

Pour évaluer l'équivalence de deux instance de rationnel, il suffit de comparer successivement leurs numérateurs et leurs dénominateurs.

Pour ordonnancer deux instances rationnel, je compare leurs numérateurs une fois exprimés sur la base d'un dénominateur commun. Notez que je ne passe pas par des instances de rationnel pour ce faire puisque cela pourrait forcer une simplification de l'un ou l'autre des rationnel, effet que nous voulons éviter ici.

   constexpr bool operator==(const rationnel &autre) const noexcept
      { return num() == autre.num() && denom() == autre.denom(); }
private:
   constexpr bool normalized_less_than
      (const rationnel &autre, value_type d) const noexcept
   {
      return num() * d / denom() < autre.num() * d / autre.denom();
   }
public:
   constexpr bool operator<(const rationnel &autre) const noexcept
   {
      return normalized_less_than(autre, ppcm(denom(), autre.denom()));
   }

Notez qu'il pourrait être pertinent de comparer d'abord les dénominateurs, car du fait que nous tenons pour invariant qu'un rationnel soit toujours en forme simplifiée, si les dénominateurs de deux instances sont distincts, il est certain que les deux instances de rationnel ne sont pas équivalentes. Il pourrait s'agir d'une optimisation intéressante.

À l'origine, j'offrais trois accesseurs utilitaires sont offerts par un rationnel sous la forme d'opérateurs de transyptage, soit les opérateurs de conversion en float, en double et en long double.

C'était redondant. J'utilise maintenant un seul opérateur, générique, avec une assertion statique pour m'assurer que le type de destination n'est pas un entier. C'est plus simple et plus général.

   template <class T>
      operator T() const noexcept
      {
         static_assert(
            !std::is_integral<T>::value,
            "La conversion n'a de sens que pour "
            " des nombres a virgule"
         );
         return static_cast<T>(num()) /
                static_cast<T>(denom());
      }
};

Pour exprimer la somme de deux instances de rationnel, je fais la somme des numérateurs une fois ceux-ci exprimés sur la base d'un dénominateur commun.

   template <class T>
      constexpr operator T() const noexcept
      {
         using std::numeric_limits;
         static_assert(
            std::is_floating_point<T>::value,
            "La conversion n'a de sens que pour des nombres a virgule"
         );
         return static_cast<T>(num()) / static_cast<T>(denom());
      }
};

Par souci de simplicité, et dans le plus pur respect du principe DRY, j'ai exprimé à travers . C'est simple et efficace, et ça pourrait être plus rapide si nous raffinions .

inline rationnel operator+(rationnel a, rationnel b)
{
   const auto d = ppcm(a.denom(), b.denom());
   return rationnel{a.num() * d / a.denom() + b.num() * d / b.denom(), d};
}
inline rationnel operator-(rationnel a, rationnel b)
   { return a + -b; }

La multiplication d'un rationnel par un autre suit essentiellement l'expression mathématique usuelle pour cette opération. Pour ce qui est de la division , elle est exprimée par la multiplication .

inline rationnel operator*(rationnel a, rationnel b)
   { return rationnel{a.num() * b.num(), a.denom() * b.denom()}; }
inline rationnel operator/(rationnel a, rationnel b)
   { return a * rationnel{b.denom(), b.num()}; }

Enfin, les opérateurs permettant respectivement de projeter un rationnel sur un flux et d'extraire un rationnel d'un flux sont déclarés ici, pour être ensuite définis dans un fichier source (rationnel.cpp, plus bas). Il ne semblait pas pertinent d'exprimer ces fonctions inline.

std::ostream& operator<<(std::ostream&, const rationnel &);
std::istream& operator>>(std::istream&, rationnel&);
#endif;

Examinons maintenant rationnel.cpp, le fichier source définissant certains services déclarés dans rationnel.h.

La projection d'un rationnel sur un flux est toute simple : il suffit de projeter son numérateur, le séparateur (ici, j'ai choisi '/') et son dénominateur, tout simplement.

#include "rationnel.h"
#include <iostream>
using namespace std;
ostream& operator<<(ostream &os, const rationnel &r)
   { return os << r.num() << rationnel::SEP << r.denom(); }

Comme c'est presque toujours le cas, la consommation d'un rationnel à partir d'un flux est plus complexe que sa contrepartie, devant tenir compte de divers cas d'erreurs possibles.

Comme il se doit, l'implémentation utilisée ici ne modifie le rationnel de destination r que si la lecture a fonctionné et si le rationnel « lu » a bel et bien pu être construit.

istream& operator>>(istream &is, rationnel &r)
{
   if (!is) return is;
   rationnel::value_type num, denom;
   if (!(is >> num)) return is;
   char c;
   if (!(is >> c) || c != rationnel::SEP)
   {
      is.putback(c);
      return is;
   }
   if (!(is >> denom)) return is;
   r = rationnel(num,denom);
   return is;
}

Reste à examiner le programme de test que j'ai utilisé, maintenant. Ce programme sera tout simple.

La stratégie globale ira comme suit :

  • une répétitive itérera jusqu'à ce qu'une erreur survienne, ce qui pourra se produire lors d'une erreur de lecture ou lors d'une division par zéro;
  • à chaque itération, un rationnel sera lu sur l'entrée standard;
  • ce rationnel sera comparé avec tous les autres rationnels consommés précédemment, et le fruit des tests réalisés sur ceux-ci sera projeté sur la sortie standard;
  • enfin, le rationnel nouvellement lu sera ajouté à la liste de ceux ayant été lus et testés.

Conséquemment, on ne verra des résultats de tests que si au moins deux instances de rationnel ont déjà été lues, et le nombre de résultat affichés croîtra linéairement à chaque itération en fonction du nombre d'instances de rationnel lues.

#include "rationnel.h"
#include <vector>
#include <algorithm>
#include <cstdlib>
using namespace std;
int main()
{
   vector<rationnel> v;

Chaque test sera réalisé par un foncteur, qui capturera le rationnel nouvellement lu à la construction et qui prendra ensuite chacune des autres instances de rationnel en paramètre à la méthode operator().

Notez qu'un test est fait avant de tenter une division d'un rationnel par un autre, et que le zéro d'un rationnel est ici un rationnel par défaut, tout simplement.

// ...
   struct test
   {
      rationnel r;
      test(const rationnel &r)
         : r(r)
      {
       }
      void operator()(const rationnel &autre) const
      {
         cout << r << " ~= " << static_cast<double>(r) << " et "
              << autre << " ~= " << static_cast<double>(autre)
              << endl;
         if (r == autre)
            cout << r << " == " << autre << endl;
         if (r != autre)
            cout << r << " != " << autre << endl;
         if (r < autre)
            cout << r << " < "  << autre << endl;
         if (r <= autre)
            cout << r << " <= " << autre << endl;
         if (r > autre)
            cout << r << " > "  << autre << endl;
         if (r >= autre)
            cout << r << " >= " << autre << endl;
         cout << r << " + " << autre << " == "
              << r + autre << endl;
         cout << r << " * " << autre << " == "
              << r * autre << endl;
         if (autre != rationnel())
            cout << r << " / " << autre << " == "
                 << r / autre << endl;
      }
   };

Enfin, le programme en tant que tel s'exprime par une répétitive inscrite dans un bloc try...catch pour fins de gestion d'exceptions.

Si une erreur de lecture survient, alors le programme se termine (appel à exit()). Un programme se terminera aussi si un rationnel lu représenterait, une fois construit, une division par zéro : ceci lèvera une exception lors de sa construction.

   try
   {
      for(;;)
      {
         rationnel r;
         cout << "Entrez un rationnel: " << flush;
         if (!(cin >> r))
            exit(0);
         for(auto & r : v) test(r);
         v.push_back(r);
      }
   }
   catch (...)
   {
      cout << "Au revoir..." << endl;
   }
}

Implémentation constexpr

Pour implémenter un rationnel qui soit constexpr, même au sens de C++ 11, il faut faire un effort de concision. Une partie des explications données pour la version « classique » plus haut ne sera pas répétée ici, par souci de concision.

Les inclusions et les outils mathématiques de base servant à simplifier les fractions demeurent les mêmes que ceux utilisés dans la version classique, à une exception près : l'en-tête relation.h déploiera ici une version constexpr des relations CRTP :

#ifndef RELATION_H
#define RELATION_H
namespace relation {
   // ... voir plus haut ...
}
namespace constexpr_relation {
   template <class T>
      struct ordonnancement {
         friend constexpr bool operator>(const T &a, const T &b)
            { return b < a; }
         friend constexpr bool operator<=(const T &a, const T &b)
            { return !(b < a); }
         friend constexpr bool operator>=(const T &a, const T &b)
            { return !(a < b); }
      };
   template <class T>
      struct equivalence {
         friend constexpr bool operator!=(const T &a, const T &b)
            { return !(a == b); }
      };
}
#endif
#ifndef RATIONNEL_H
#define RATIONNEL_H

#include "relation.h"
#include <type_traits>
#include <cmath>
#include <limits>
#include <functional>

class division_par_zero {};

template <class T>
   constexpr T raw_pgcd(T a, T b)
   {
      return b ? raw_pgcd(b, a % b) : a;
   }
template <class T>
   constexpr T pgcd(T a, T b)
   {
      return !a && !b? throw division_par_zero{} : raw_pgcd(a,b);
   }
template <class T>
   constexpr T ppcm(T a, T b)
      { return a * b / pgcd(a,b); }
template <class T>
   constexpr T absolute_value(T val)
      { return val < 0? -val : val; }

Le défi est de construire un rationnel de manière constexpr, donc de simplifier les fractions ainsi représentées à la compilation lorsque les paramètres sont eux-mêmes connus à la compilation.

Les outils privés suivants sont aussi constexpr, soit :

  • La méthode pick_num_sign(), qui détermine le signe du numérateur sur la base des signes suppléés pour le numérateur et pour le dénominateur
  • La méthode compute_denom(), qui détermine la valeur du dénominateur sur la base des valeurs supplééees pour le numérateur et pour le dénominateur, et
  • La méthode compute_num(), qui détermine la valeur du numérateur sur la base des valeurs supplééees pour le numérateur et pour le dénominateur

Constatez ici que l'implémentation constexpr peut être légèrement inefficace dans le cas de valeurs connues à l'exécution, car les méthodes compute_num() et compute_denom() contiennent des calculs (récursifs!) redondants. Je pense toutefois que le jeu en vaut la chandelle pour un type tel que rationnel.

class rationnel
   : constexpr_relation::equivalence<rationnel>,
     constexpr_relation::ordonnancement<rationnel>
{
public:
   using value_type = int;
   static constexpr char SEP = '/';
private:
   value_type num_, denom_;
   static constexpr value_type pick_num_sign(value_type num, value_type denom) {
      return num < 0 == denom < 0? absolute_value(num) : -absolute_value(num);
   }
   static constexpr value_type compute_denom(value_type num, value_type denom) {
      return !denom ? throw division_par_zero{} :
             !num ? 1 : absolute_value(denom) / pgcd(absolute_value(num), absolute_value(denom));
   }
   static constexpr value_type compute_num(value_type num, value_type denom) {
      return !num ? num : pick_num_sign(num, denom) / pgcd(absolute_value(num), absolute_value(denom));
   }

Le constructeur par défaut est constexpr de manière triviale. Le réel défi est le constructeur paramétrique, qui repose sur les outils privés susmentionnés.

Les méthodes num() et denom() sont trivialement constexpr. Il en va de même pour l'opérateur + unaire.

L'opérateur - unaire est aussi simple à implémenter de manière constexpr dans la mesure où le constructeur paramétrique l'est aussi. Du fait que le type rationnel proposé ici implémente le signe dans le numérateur, il suffit de construire un autre rationnel en permutant le signe de *this.

Enfin, l'opérateur == repose strictement sur une comparaison des numérateurs et des dénominateurs, donc est trivialement constexpr lui aussi.

public:
   constexpr rationnel(value_type num = {})
      : num_{num}, denom_{1}
   {
   }
   rationnel(float) = delete;
   rationnel(double) = delete;
   rationnel(long double) = delete;
   constexpr rationnel(value_type num, value_type denom)
      : num_{ compute_num(num, denom) }, denom_{ compute_denom(num, denom) }
   {
   }
   constexpr value_type num() const noexcept
      { return num_; }
   constexpr value_type denom() const noexcept
      { return denom_; }
   constexpr rationnel operator+() const noexcept
      { return *this; }
   constexpr rationnel operator-() const noexcept
      { return rationnel{-num(), denom()}; }
   constexpr bool operator==(const rationnel &autre) const noexcept
      { return num() == autre.num() && denom() == autre.denom(); }

L'opérateur < est implémenté sur la base d'une méthode privée normalized_less_than(), qui place les deux instances de rationnel en comparaison sur la base de leur plus petit commun multiple, ce qui permet de limiter les appels à ppcm() à un seul. Les deux services sont constexpr, bien sûr.

private:
   constexpr bool normalized_less_than(const rationnel &autre, value_type d) const noexcept
   {
      return num() * d / denom() < autre.num() * d / autre.denom();
   }
public:
   constexpr bool operator<(const rationnel &autre) const noexcept
   {
      return normalized_less_than(autre, ppcm(denom(), autre.denom()));
   }

La conversion en un T se fait sur demande seulement (opérateur de conversion qualifié explicit), dans la mesure où T est un type à virgule flottante, et se résout par une division du numérateur par le dénominateur, tous deux évalués de manière constexpr.

   template <class T>
      explicit constexpr operator T() const noexcept
      {
         static_assert(
            std::is_floating_point<T>::value,
            "La conversion n'a de sens que pour des nombres a virgule"
         );
         return static_cast<T>(num()) / static_cast<T>(denom());
      }
};

Pour les opérateurs +, -, * et / binaires, l'expression constexpr des opérateurs est triviale dans la mesure où le constructeur paramétrique est constexpr. Je n'ai pas implémenté l'opérateur %, n'en ayant pas besoin, mais je suis ouvert aux suggestions si vous avez envie de vous attaquer à ce problème.

constexpr rationnel operator+(rationnel a, rationnel b)
{
   return rationnel{
      a.num() * ppcm(a.denom(), b.denom()) / a.denom() + b.num() * ppcm(a.denom(), b.denom()) / b.denom(),
      ppcm(a.denom(), b.denom())
   };
}
constexpr rationnel operator-(const rationnel &a, const rationnel &b)
   { return a + -b; }
constexpr rationnel operator*(rationnel a, rationnel b)
   { return rationnel{a.num() * b.num(), a.denom() * b.denom()}; }
constexpr rationnel operator/(rationnel a, rationnel b)
   { return a * rationnel{b.denom(), b.num()}; }
#include <iosfwd>
std::ostream& operator<<(std::ostream&, const rationnel&);
std::istream& operator>>(std::istream&, rationnel&);
#endif

Je n'ai pas répété le code des opérateurs de lecture et d'écriture sur un flux, du fait qu'il demeure inchangé. Référez-vous à la version « classique » pour plus de détails.

Parmi les raisons pour faire du type rationnel un type pleinement constexpr, on trouve bien sûr la qualité du code généré, avec une part importante du code résolu de manière statique, mais aussi la capacité de valider le code sur la base d'assertions statiques. Par exemple :

#include "rationnel.h"
#include <iostream>
void tester_rationnels(rationnel r0, rationnel r1) {
   cout << r0 << " + " << r1 << " == " << (r0 + r1) << endl;
   cout << r0 << " - " << r1 << " == " << (r0 - r1) << endl;
   cout << r0 << " * " << r1 << " == " << (r0 * r1) << endl;
   cout << r0 << " / " << r1 << " == " << (r0 / r1) << endl;
   cout << "-" << r0 << " == " << -r0 << endl;
   cout << "+" << r0 << " == " << +r0 << endl;
   cout << "-" << r1 << " == " << -r1 << endl;
   cout << "+" << r1 << " == " << +r1 << endl;
}
int main()
{
   static_assert(rationnel{ 1 } + rationnel{ 1 } == rationnel{ 2 }, "");
   static_assert(rationnel{ 1 } - rationnel{ 1 } == rationnel{ 0 }, "");
   static_assert(rationnel{ 2 } - rationnel{ 4 } == rationnel{ -2 }, "");
   static_assert(rationnel{ -2 } * rationnel{ -4 } == rationnel{ 8 }, "");
   static_assert(rationnel{ 8 } / rationnel{ 4 } == rationnel{ 2 }, "");
   static_assert(rationnel{ 8, 4 } == rationnel{ 2 }, "");
   static_assert(rationnel{ -3, -1 } == rationnel{ 3 }, "");
   static_assert(rationnel{ 3, 5 } * rationnel{ 2 } == rationnel{ 6, 5 }, "");
   // ...
}

Ceci transforme les tests unitaires en une compilation : le code ne compile que s'il respecte les attentes placées en lui.

En espérant que tout cela vous soit utile!


Valid XHTML 1.0 Transitional

CSS Valide !