Processing math: 100%

En lien avec la « prochaine minute »

Notez que j'utilise constexpr dans mes réponses plus bas, mais que ce concept n'avait pas été présenté en classe au moment où Q02 fut fait. Il est donc normal que ce mot ne soit ni dans l'énoncé qui vous a été présenté, ni dans vos réponses. Cela dit, bien que le code fonctionne sans ce mot, il est nettement de meilleure qualité avec ce mot.

Le minitest Q02 présentait un synopsis d'une classe Minute tel que proposé ci-dessous.Une instance de cette classe devait représentera une minute, dont les valeurs sont des entiers pouvant se situer entre 0 et 59 inclusivement :

class HorsBornes {};
class Minute {
public:
   using value_type = short;
private:
   value_type n{};
public:
   Minute() = default;
   constexpr value_type valeur() const noexcept {
      return n;
   }
   static constexpr value_type minval() noexcept {
      return 0;
   }
   static constexpr value_type maxval() noexcept {
      return 59;
   }
   constexpr Minute(value_type);
   // etc.
};

Première question

La première question du minitest commençait par ce préambule : « En vertu de l'encapsulation, un objet une fois construit se trouve dans un état utilisable et respectant les invariants de sa classe ». Le libellé de la question demandait ensuite : « complétez le constructeur paramétrique de Minute de manière à ce qu'une valeur ne respectant pas l'invariant susmentionné mène à une levée d'un HorsBornes ».

Une réponse correcte à cette question serait celle-ci :

Minute::Minute(value_type val) : n{ minval() <= val && val <= maxval()? val : throw HorsBornes{} } {
}

En pratique, nous avons ici un bel exemple d'une classe dont les méthodes pourraient toutes être qualifiées constexpr, incluant le constructeur.

Notez qu'un objet dont la construction ne s'est pas complétée n'a jamais été construit, et ne sera conséquemment jamais détruit. Certains ont fait remarquer (avec justesse) qu'une autre des questions du minitest aurait été utile pour répondre à cette dernière.

Deuxième question

Le libellé de la question suivante était : « complétez la spécialisation de numeric_limits pour le type Minute en y ajoutant les méthodes min() et max() ». Une réponse possible était la suivante :

namespace std {
   //
   // Version exprimant les min et max à partir du value_type
   // exposé en tant que type interne et public à Minute
   //
   template <>
      struct numeric_limits<Minute> {
         static constexpr Minute::value_type min() noexcept {
            return Minute::minval();
         }
         static constexpr Minute::value_type max() noexcept {
            return Minute::maxval();
         }
      };
}

Notez qu'il est illégal d'ajouter des membres à l'espace nommé std, qui est réservé aux gens qui assurent la gestion du standard, mais qu'il est possible d'en spécialiser des fonctions ou des types génériques pour des cas particuliers. C'est ce que nous faisons ici. Il n'était pas nécessaire de savoir cela pour répondre à la question, mais la culture, ça n'a pas de prix.

Retourner des instances de Minute aurait aussi été correct, mais aurait eu un impact sur notre capacité de réinvestir le code directement, du moins avec une classe Minute aussi simple (pour ne pas dire simpliste) que celle proposée dans l'énoncé plus haut. Nous y reviendrons :

namespace std {
   //
   // Variante correcte, mais qui implique quelques changements
   //
   template <>
      struct numeric_limits<Minute> {
         static constexpr Minute min() {
            return Minute::minval();
         }
         static constexpr Minute max() {
            return Minute::maxval();
         }
      };
}

Troisième question

Le libellé de la troisième et dernière question ressemblait à ceci :

Rédigez la méthode de classe (static) nommée prochaine(Minute) de la classe Minute retournant la prochaine Minute légale suivant celle reçue en paramètre.

Par exemple :

int main() {
   Minute m(58);
   m = Minute::prochaine(m); // 59
   m = Minute::prochaine(m); // 0
   m = Minute::prochaine(m); // 1
   // ...
}

J'ai beaucoup apprécié l'exercice, cela dit, puisque les réponses proposées en classe furent variées. La plupart fonctionnent, ou fonctionnent presque, mais il y a presque toujours un petit quelque chose à dire sur ce qui a été proposé... Nous ferons donc un peu de pédagogie sur la base de vos propositions.

Tout d'abord, ma réponse personnelle aurait été la suivante (on présume ici qu'elle a été déclarée convenablement dans Minute au préalable, bien entendu) :

constexpr Minute Minute::prochaine(Minute m) noexcept {
   return { (m.valeur() + 1) % (maxval() + 1) + minval() };
}

Notez que la borne supérieure utilisée est maxval + 1 du fait que la borne décrite par Minute::maxval() est inclusive. On aurait aussi pu exprimer cette borne à partir de traits, mais dans ce cas bien précis nous parlons strictement de Minute, qui est un type concret, pas un type générique, donc s'en tenir aux méthodes de Minute suffit.

J'aurais pu passer m en paramètre référence-vers-const plutôt que par valeur, mais dans ce cas-ci la copie est essentiellement gratuite.

Sur le plan syntaxique, notez que une méthode de classe est qualifiée static à sa déclaration (voir plus bas) mais pas lors de sa définition si les deux (déclaration et définition) sont faites en deux lieux distincts. Notez aussi l'absence de qualification const, du fait que celle-ci s'applique à this et que, dans une méthode de classe, il n'y a pas de this.

Si cette méthode elle avait été définie à même la déclaration de la classe Minute, on aurait pu écrire :

class HorsBornes {};
class Minute {
public:
   using value_type = short;
private:
   value_type n{};
public:
   Minute() = default;
   constexpr value_type valeur() const noexcept {
      return n;
   }
   static constexpr value_type minval() noexcept {
      return 0;
   }
   static constexpr value_type maxval() noexcept {
      return 59;
   }
   constexpr Minute(value_type val) : n{ minval() <= val && val < maxval()? val : throw HorsBornes{} } {
   }
   constexpr bool operator<(const Minute &m) const noexcept {
      return valeur() < m.valeur();
   }
   constexpr bool operator==(const Minute &m) const noexcept {
      return valeur() == m.valeur();
   }
   static constexpr Minute prochaine(Minute m) noexcept {
      return { (m.valeur() + 1) % (maxval() + 1) + minval() };
   }
   // ... reste à exprimer !=, <=, > et >= (amusez-vous)...
};

Diverses propositions faites en réponse à la question

Ce qui suit liste des propositions faites en classe par divers individus. J'ai trouvé les propositions suffisamment fécondes pour qu'il vaille la peine de les examiner et de clarifier leurs bons et leurs moins bon côtés. Je ne discuterai que des propositions qui différent de ce qui, à mes yeux, serait adéquat ou « collerait » à l'idiomatique attendue.

Si la pédagogie à partir de bons ou de mauvais coups est une approche qui vous sied, vous aimerez peut-être la section Musée des horreurs de ce site.

Qu'on se comprenne bien : l'idée ici est d'examiner les petits trucs qui accrochent ou qui sont perfectibles, pour s'améliorer collectivement et enrichir notre compréhension de la syntaxe, de la sémantique et de l'idiomatique de C++, tout en examinant diverses façons de faire pour résoudre un seul et même problème, et non pas de rire des gens. Vous constaterez d'ailleurs que la présentation est entièrement anonyme, pour des raisons évidentes, et que les propositions sont identifiées par des lettres (aucun ordre particulier sinon celui où les feuilles étaient dans la pile quand j'ai corrigé le tout).

Les remarques faites dans chaque cas seront, pour la plupart, identifiées par une note de la forme R x x0 ; dans le but de réduire la redondance dans le texte, quand un élément apparaîtra à plus d'une occasion, vous trouverez un renvoi à son occurrence originale.

Remarques globales

J'ai regroupé dans cette section des remarques d'ordre général, applicables à la plupart des propositions de solution plus bas.

R00 – Une méthode de classe, qualifiée static en C++ comme en Java ou en C#, appartient à la classe dans son ensemble, pas à une instance en particulier. Cela signifie qu'il n'y a pas de this dans une telle méthode. Pour cette raison, elle ne pourrait être qualifiée const, du fait que la mention const apposée à une méthode s'applique précisément à this.

R01 – Lorsqu'on passe une donnée d'une type autre que primitif en paramètre à une fonction ou à une méthode, si cette donnée n'est pas destinée à être modifiée par la fonction ou la méthode en question, on tend à éviter le passage par valeur et à privilégier le passage par référence-vers-const. L'idée est d'épargner le coût de la construction et de la destruction de la temporaire qui sera créée par la mécanique de copie. Cependant, dans le cas d'une Minute, le constructeur ne copie qu'un entier, et le destructeur est trivial au sens mathématique du terme. Ainsi, en théorie, le passage par valeur ne devrait essentiellement rien coûter (en pratique, il faudrait tester le tout sur le compilateur choisi pour s'assurer de faire un choix judicieux).

R02 – En vertu de l'encapsulation, un objet déjà construit est présumé valide, au sens où ses invariants sont respectés. Ainsi, on ne devrait pas avoir à valider les états d'un objet existant déjà, sa construction faisant implicitement office de validité.

R03 – Le constructeur paramétrique de Minute est un constructeur implicite, ce qui fait que la conversion d'un entier à une Minute est un automatisme pris en charge par le moteur d'inférence de types. L'opération inverse n'est pas contre pas offerte, du moins si l'on se fie à la description de la classe Minute. On pourrait bien sûr ajouter à cette classe la méthode operator value_type() const, et si le recours à cet opérateur avait été explicité dans les propositions de réponses, j'en aurais tenu compte).

Proposition A

Examinons la proposition.

static Minute prochaine(Minute m) {
   if(m == maxval())
      m.n = minval();
   else
      m.n++;
   return m;
}

Cette proposition utilise le passage par valeur (R01) et la construction implicite d'une Minute à partir d'un Minute::value_type (R03) pour la comparaison m == maxval().

R04 – Notez que l'expression m.n++, bien que correcte, est en général moins efficace que l'expression ++(m.n). Les opérateurs ++ et -- préfixés opèrent sur l'objet lui-même, alors que leur version postfixée (ou suffixée) implique la construction et la destruction d'une temporaire.

R05 – En général, on évite de modifier l'objet reçu en paramètre, mais dans ce cas-ci, l'objet est une copie locale, sorte de variable temporaire, alors tout est propre.

Cette proposition fonctionne.

Proposition B

Examinons la proposition.

static Minute prochaine(const Minute &m) {
   if (m.valeur() <= m.maxval())
      return Minute(m.minval());
   return Minute(++m.valeur());
}

R06 – Cette fonction a deux points de sortie (deux return). Sans que ce ne soit foncièrement mal, c'est une pratique qui complique parfois l'analyse des programmes (ici, on s'entend, la fonction est toute simple). Je vous suggère d'éviter cela, sauf :

R07 – Il est illégal d'appeler ++ sur un rvalue, à ma connaissance, et m.valeur() retourne un Minute::value_type par valeur; tant que ce qui est retourné n'est pas déposé dans un lvalue non-const (dans un objet nommé), il est (je pense) illégal de faire ++ ou -- sur lui. Je marche sur des oeufs ici, cependant, car je ne suis pas certain des règles de C++ 11 en lien avec les références sur des rvalue. Dans ce cas bien précis, on aurait par contre pu remplacer ++m.valeur(), qui est discutable, par m.valeur()+1, et cela aurait eu l'avantage non-négligeable d'envoyer un message plus précis.

Cette proposition fonctionne presque.

Proposition C

Examinons la proposition.

static Minute& prochaine(Minute &m) {
   ++m.n;
   return m;
}

Cette proposition a trois vilains défauts :

R08 – Vous connaissez peut-être ce qu'on nomme le principe de moindre surprise, qui nous rappelle que les gens qui utilisent une API s'attendent à ce que celle-ci fonctionne de manière prévisible (certains diraient « compréhensible »). Dans ce cas bien précis, il est probable que la majorité des programmeurs sollicitant Minute::prochaine() pour la première fois ne s'attendront pas à ce que cete méthode modifie l'état de son paramètre.

À propos de troisième défaut ci-dessus, notez que cela réduit l'applicabilité de la méthode. En effet, seuls les lvalue non-const peuvent être utilisés avec cette implémentation, donc les deux appels proposés à droite sont illégaux dans ce cas-ci, et pourtant on serait en droit de s'attendre à ce qu'ils soient tout à fait recevables.

const Minute debut = Minute::minval();
// illégal: debut est const
Minute m0 = Minute::prochaine(debut);
// illégal: Minute(3) est un rvalue
Minute m1 = Minute::prochaine(Minute(3));

Cette proposition ne fonctionne donc pas en général.

Proposition D

Examinons la proposition.

static prochaine(Minute laminute) {
   static_assert {
      assert(valider<Minute>(laminute.n++);
   };
   return *this;
}

Sans entrer dans les détails, notons quelques problèmes :

Cette proposition ne fonctionne pas.

Proposition E

Examinons la proposition.

static Minute::value_type prochaine(const Minute &minute) {
   Minute::value_type val(++minute.valeur());
   if (val <= Minute::max())
      return val;
   else
      return Minute::min();
}

Pour le problème en lien avec ++ sur un rvalue, voir R07. Pour le double point de sortie, voir R06. Évidemment, les méthodes de Minute se nomment minval() et maxval() dans l'énoncé à partir duquel nous travaillons ici, pas min() et max().

Cette proposition ne fonctionne pas, mais elle pourrait fonctionner avec de légères modifications.

Proposition F

Examinons la proposition.

value_type Minute::prochaine(value_type val) {
   if (++val > Minute::maxval())
      return Minute::minval();
   else
      return val;
}

L'énoncé passait une Minute en paramètre à Minute::prochaine(), pas une Minute::value_type, et nous n'avons pas défini d'opérateur de conversion en Minute::value_type pour Minute. Pour le double point de sortie, voir R06.

Cette proposition ne fonctionne pas, mais elle pourrait fonctionner avec de légères modifications.

Proposition G

Examinons la proposition.

static Minute prochaine(Minute minute) {
   Minute minu;
   if (minute.valeur() == Minute::maxval())
      minu = Minute(Minute::minval());
   else
      minu = Minute(minute.valeur() + 1);
   return minu;
}

Une petite remarque ici tient au fait qu'elle dépend de l'existence d'un constructeur par défaut pour une Minute. Cela dit, ce constructeur est défini dans notre cas.

Cette proposition fonctionne.

Proposition H

Examinons la proposition.

static Minute minute::prochaine(Minute m) {
   return Minute((m.valeur() % (Minute::maxval() - Minute::minval()) + Minute::minval());
}

Cette solution rejoint celle que j'ai moi-même proposé plus haut, sauf pour le fait qu'il manque un petit + 1 pour aller à la prochaine Minute, justement.

Cette proposition fonctionne presque.

Proposition I

Examinons la proposition.

static Minute prochaine(Minute &minute) {
   if (valider<Minute>(minute.valeur()+1))
      return Minute(minute.valeur() + 1);
   else
      return Minute(Minute::minval());
}

Pour le double point de sortie, voir R06. Notez que minute est passé par référence sans qualification const, donc que la méthode pourrait modifier cet objet (elle ne le fait pas, cela dit, respectant le principe de moindre surprise – voir R08 – ce qui est sage ici). La qualification const manquante réduit l'utilité de l'interface de la méthode; il serait sage d'y remédier.

Cette proposition fonctionne.

Proposition J

Examinons la proposition.

value_type prochaine(value_type m) {
   return (++m) % 60;
}

Bien que la mathématique de la chose soit convenable sur le plan opératoirem du moins dans notre cas où les valeurs vont de 0 à 59 inclusivement, la fonction montre un certain nombre de problèmes. Entre autres :

Cette proposition ne fonctionne pas.

Proposition K

Examinons la proposition.

static Minute prochaine(const Minute &m) const noexcept {
   return Minute(m.valeur() == numeric_limits<Minute>::max()? 0 : m.valeur() + 1);
}

Du fait qu'il s'agit d'une méthode de classe, cette méthode ne peut être qualifiée const (R00). Idéalement, on remplacerait le littéral 0 par numeric_limits<Minute>::min(). Outre cela, elle est très correcte.

Cette proposition fonctionne presque.

Proposition L

Examinons la proposition.

static value_type prochaine(const Minute m) {
   if(m.valeur() + 1 > Minute::maxval())
      return Minute::minval();
   else
      return m.valeur() + 1;
}

Pour le double point de sortie, voir R06. Notez que les deux points de sortie utilisent le constructeur paramétrique implicite décrit à R03. Passer un paramètre par valeur et const n'apporte pas grand-chose, mais ce n'est pas un bogue ici.

Cette proposition fonctionne.

Proposition M

Examinons la proposition.

static Minute prochaine(Minute min) const noexcept {
   if (min.valeur() == minute::maxval()) {
      Minute m = new Minute();
      return m;
   } else
      return new minute(min.valeur() + 1);
}

Du fait qu'il s'agit d'une méthode de classe, cette méthode ne peut être qualifiée const (R00). Pour le double point de sortie, voir R06. Notez que la classe est parfois nommée Minute, et parfois nommée minute.

Cette proposition a ceci de particulier qu'elle confond objets, créés automatiquement, et références sur des objets, comme le veut la pratique en Java ou dans les langages .NET. En C++, on utilise l'allocation dynamique de ressources sur des pointeurs, de un, et si nécessaire, de deux, contrairement à d'autres langages où il n'y a pas de réelle alternative.

Cette proposition ne fonctionne pas, mais pourrait être ajustée pour fonctionner.

Proposition N

Examinons la proposition.

static Minute prochaine(Minute minute) {
   return ((minute + 1) % Minute(maxval));
}

L'idée derrière cette méthode se tient, mais exigerait quelques opérateurs supplémentaires pour la classe Minute. En effet :

Cette proposition ne fonctionne pas, mais pourrait fonctionner si on ajoutait les opérateurs + et % correctement définis à la classe Minute.

Proposition O

Examinons la proposition.

static value_type prochaine(const Minute &val) {
   value_type temp = val.n + 1;
   try {
      Minute m = temp;
      return m.valeur();
   } catch(HorsBornes) {
      temp = 0;
   }
   return temp;
}

Cette proposition fonctionne mais pourrait être légèrement plus efficace (on pourrait y omettre le if). Elle procède en effet par essai-erreur, examinant la possibilité de construire une Minute avec une Minute::value_type puis, dans l'éventualité où cela ne s'avérerait pas approprié, y allant avec un plan B.

Son plus gros « problème » est qu'elle retourne non pas une Minute mais bien une Minute::value_type, ce qui contrevient quelque peu à la consigne. C'est simple à corriger, cependant : il suffit de changer le type retourné par la fonction et le constructeur paramétrique implicite se chargera de créer automatiquement une Minute à partir de la valeur temp.

Cette proposition fonctionne.

Proposition P

Examinons la proposition.

static Minute prochaine(Minute min) {
   value_type n_;
   n_ = min::valeur();
   n_ = n_ + 1;
   if (n <= min::maxval())
      return Minute(n_);
   else
      return Minute(min::minval());
}

Pour le double point de sortie, voir R06. On note une confusion syntaxique ici entre solliciter le service d'une classe à partir de son type (p. ex. : Minute::maxval()) et solliciter des services à partir d'une de ses instances (p. ex. : min.maxval()). Notez la nuance entre l'opérateur :: et l'opérateur . dans ces deux exemples.

Petit détail de stylistique : étant donné que le mot min est traditionnellement associé à la recherche du minimum d'au moins deux valeurs, je chercherais un autre nom pour une variable comme celle-ci si j'étais vous (m, minu, minute, peu importe). Aussi, les usages sont à l'effet d'utiliser des noms suffixés par _ (comme n_) pour les attributs d'instances; pour une variable locale comme n_ ici, un autre nom (peut-être aussi simple que n) serait à privilégier.

R09 – Notez que les trois premières opérations pourraient n'en être qu'une seule (value_type = min.valeur() + 1), ce qui serait sain. Dans les langages OO, on cherche habituellement à construire les objet une fois seulement que les paramètres requis pour le construire sont connus, dans un but d'efficience (on ne souhaite pas créer un objet inutile puis remplacer ses états par la suite).

Outre le problème syntaxique en lien avec l'opérateur ::, cette proposition fonctionne.

Proposition Q

Examinons la proposition.

Minute prochaineMinute(Minute m) const {
   return Minute ((m.valeur + 1) % (numeric_limits<Minute>::max + 1));
}

Le nom de la fonction ne respecte pas la consigne. Du fait qu'il s'agit d'une méthode de classe, cette méthode ne peut être qualifiée const (R00).

Notez que les appels à la méthode valeur() de la Minute nommée m et et la méthode max() du type numeric_limits<Minute> nécessiteraient des parenthèses. Notez aussi qu'on a habituellement recours aux traits pour rédiger du code générique, mais qu'ici on sait pertinemment qu'on parle des bornes d'une Minute alors utiliser maxval() serait suffisant.

Cette proposition ne fonctionne pas, mais pourrait être ajustée (syntaxiquement) pour fonctionner.

Proposition R

Examinons la proposition.

static Minute prochaine(const Minute m) {
   value_type val = m.valeur() - minval();
   return Minute (((val + 1) % (maxval() - minval() + 1)) + minval());
}

Rien à dire, pour être honnête. C'est propre et général.

Cette proposition fonctionne.

Proposition S

Examinons la proposition.

static Minute prochaine(Minute m) {
   assert(m < numeric_limits::min() && m >= numeric_limits::max());
   return m + 1;
}

En vertu de l'encapsulation, m est présumé correct a priori (R02), donc l'assertion dynamique ne devrait pas être requise. Si nous souhaitions avoir recours à une assertion dynamique, il faudrait réécrire les utilisations du trait numeric_limits pour les appliquer au type Minute ou les remplacer par des appels à minval() et à maxval() tout simplement.

Le caractère cyclique de la valeur d'une Minute n'est pas respecté ici (une simple incrémentation ne suffit pas).

Cette proposition ne fonctionne pas.

Proposition T

Examinons la proposition.

static Minute prochaine(Minute m) {
   Minute next;
   if(m.valeur() % maxvalue()) == 0) {
      next = Minute(minvalue());
   } else {
      next = Minute(++m.valeur());
   }
   return next;
}

En vertu de l'encapsulation, m est présumé correct a priori (R02), donc la condition du if est une tautologie. Pour le problème en lien avec ++ sur un rvalue, voir R07. Les noms des méthodes décrivant les bornes (minvalue(), maxvalue()) ne correspondent pas à ceux dans l'énoncé du problème (minval(), maxval()), mais c'est un moindre mal.

Cette proposition ne fonctionne pas.

Proposition U

Examinons la proposition.

static value_type prochaine(value_type m) noexcept {
   return m >= maxval()? 0 : m++;
}

Celle-ci est un cas intéressant. Elle ne respecte pas le type de retour attendu, et devrait utiliser minval() plutôt que 0, mais son plus gros bogue est que m++ incrémente m, mais retourne la valeur avant incrémentation. Conséquemment, cette méthode ne retourne pas la prochaine Minute.

Cette proposition ne fonctionne pas.

Proposition V

Examinons la proposition.

static prochaine(value_type m) {
   if(m == maxval())
      return 0;
   else if (m <= minval() && m <= (maxval() - 1))
      return m + 1;
   else
      return HorsBornes();
}

Celle-ci fait plusieurs trucs étranges, dont oublier le type de retour et confondre return et throw. Je l'ai conservée toutefois surtout parce qu'elle fait des démarches pour éviter de construire une Minute (du moins, on le présume en l'absence du type de retour) avec une valeur hors bornes... alors que le même effet serait atteint en construisant une Minute et en laissant le constructeur faire son travail, donc lever une exception si la valeur reçue en paramètre devait ête hors bornes.

Cette proposition ne fonctionne pas.

Proposition W

Examinons la proposition.

static Minute prochaine(Minute m) noexcept
{
   try
   {
      return Minute(m.valeur() + 1);
   }
   catch(HorsBornes &e)
   {
      return Minute(Minute::minval());
   }
}

C'est une proposition intéressante, car elle est correcte sur le plan technique : noexcept est approprié, le calcul est bon, l'encapsulation est respectée en tout point, et la fonction retourne chaque fois la bonne valeur. Elle attrape l'exception par référence ce qui est sage (le nom e n'est pas nécessaire du fait que la variable n'est pas utilisée). Cependant... elle est très lente. En effet, en pratique, un programme correct ne devrait à peu près jamais lever d'exception, ce qui fait que les compilateurs optimisent fortement les blocs try... alors que les blocs catch sont extrèmement lents. Ici, on passera souvent dans le catch car on utilise la gestion d'exception à titre de structure de contrôle, ce que je vous recommande d'éviter... même si ça fonctionne!

Cette proposition fonctionne.


Voilà!


Valid XHTML 1.0 Transitional

CSS Valide !