ProfQuiz 02 – Pour les profs...

Pour les détails quant à cette démarche et pour des liens vers les autres questions : index.html#profquiz.

Pour un rappel des principes : index.html#profquiz_principes

Pour cette question, les solutionnaires suivants sont disponibles :

Question 02

Cette question fut proposée le 20 mai 2013.

Rédigez un programme qui :

Il est essentiel que le programme ne laisse fuir aucune ressource. Vous comprendrez que ce problème pourrait être proposé en deuxième session du DEC en informatique (technique ou SIM), alors portez attention aux connaissances requises de la part des étudiantes et des étudiants, de même qu'aux concepts mis de l'avant dans votre solution et de ce vers quoi ils peuvent mener votre enseignement par la suite.

Source : ceci est un exemple classique (et mal enseigné) dans la littérature... Mais vous pouvez faire mieux, n'est-ce pas?

Solution possible

Ce problème se prête à une pédagogie pour des étudiantes et des étudiants ayant eu au moins un premier contact avec la programmation orientée objet, du fait qu'il met en scène de l'héritage et du polymorphisme. Le recours à un nombre a priori inconnu d'objets de divers types implique aussi un recours à une forme ou l'autre d'allocation dynamique de mémoire. Pour cette raison, je suppose ici que la gestion des erreurs d'entrée/ sortie de part et d'autre n'est pas un obstacle.

La solution proposée dans chaque cas ci-dessous suit la même forme, indépendamment du langage utilisé. Cette forme est expliquée ici pour éviter la redondance :

Nom en C# Nom en C++Explication

AfficherTitreEtape()

afficher_titre_etape()

Formate une chaîne de caractères dans un cadre. Cette fonction est perfectible, mais j'ai laissé la même logique décorative dans chaque exemple

LireEntierPositif()

lire_entier_positif()

Lit une entrée au clavier et la valide. La condition de validité est que l'entrée représente un entier strictement positif. Nuance entre les implémentations : celle en C# lit une ligne à la fois alors que celle en C++ lit un mot à la fois

LireEntierBorne()

lire_entier_borne()

Lit une entrée au clavier et la valide. La condition de validité est que l'entrée représente un entier situé inclusivement entre deux bornes. La cohérence des bornes est validée. Nuance entre les implémentations : celle en C# lit une ligne à la fois alors que celle en C++ lit un mot à la fois

AfficherMenu()

afficher_menu()

Affiche des options de menu tirées d'une séquence de chaînes de caractères, et leur associe un numéro. Les numéros affichés sont dans pour une séquence de éléments

LireChoix()

lire_choix()

Lit et valide un choix parmi des options de menu tirées d'une séquence de chaînes de caractères. Le choix doit se trouver dans pour une séquence de éléments

Forme

Forme

Classe abstraite à partir de laquelle s'articule la logique du programme. Toute forme sait se dessiner.

Carré

Carre

Dérivé de Forme. Se dessine en affichant son nom

Rectangle

Rectangle

Dérivé de Forme. Se dessine en affichant son nom

Triangle

Triangle

Dérivé de Forme. Se dessine en affichant son nom

LireForme()

lire_forme()

Offre à l'usager un choix parmi les formes disponibles, lit et valide son choix, puis construit et retourne la forme choisie

LireNbFormes()

lire_nb_formes()

Lit et valide le nombre de formes à créer

LireFormes()

lire_formes()

À partir du nombre de formes désiré, crée un conteneur de formes, y dépose chacune des formes choisies par l'usager, et retourne le conteneur une fois celui-ci rempli

Main()

main()

Réalise le travail demandé pour cette question

Les affichages dans chaque cas sont identiques (aux accents près; lorsque l'on travaille en mode console avec C#, la mise en place de la gestion des accents est automatisée alors que ce n'est pas le cas avec C++, mais j'ai estimé que c'était secondaire ici – si vous le jugez important, il existe des solutions toutes faites).

Solution possible – C# et tableau

Une solution possible en C# serait la suivante :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace z10
{
   class Program
   {
      static void AfficherTitreÉtape(string titre)
      {
         const int LARGEUR_LIGNE = 70;
         int largeurCadreCentral = (LARGEUR_LIGNE - 2 - titre.Length) / 2;
         Console.WriteLine(new string('-', LARGEUR_LIGNE));
         Console.WriteLine("{0} {1} {0}", new string('-', largeurCadreCentral), titre); // perfectible
         Console.WriteLine(new string('-', LARGEUR_LIGNE));
      }
      static int LireEntierPositif()
      {
         int nbLu = -1;
         do
         {
            try
            {
               Console.Write("Entrez un entier strictement positif : ");
               nbLu = int.Parse(Console.ReadLine());
            }
            catch (FormatException)
            {
            }
         }
         while (nbLu <= 0);
         return nbLu;
      }
      //
      // Note: les bornes sont inclusives
      //
      static int LireEntierBorné(int minVal, int maxVal)
      {
         if (maxVal < minVal)
            throw new ArgumentException(string.Format("Méthode LireEntierBorné(), maxVal = {0} et minVal = {1}", maxVal, minVal));
         bool ok = false;
         int nbLu = 0;
         do
         {
            try
            {
               Console.Write("Entrez une valeur entière entre {0} et {1} inclusivement : ", minVal, maxVal);
               nbLu = int.Parse(Console.ReadLine());
               if (minVal <= nbLu && nbLu <= maxVal)
                  ok = true;
            }
            catch (FormatException)
            {
            }
         }
         while (!ok);
         return nbLu;
      }
      static void AfficherMenu(string[] options)
      {
         for (int i = 0; i < options.Length; ++i)
         {
            Console.WriteLine("{0} : {1}", i + 1, options[i]);
         }
      }
      static int LireChoix(string[] options)
      {
         return LireEntierBorné(1, options.Length);
      }
      abstract class Forme
      {
         public abstract void Dessiner();
      }
      class Carré : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Carré");
         }
      }
      class Rectangle : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Rectangle");
         }
      }
      class Triangle : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Triangle");
         }
      }
      static Forme LireForme()
      {
         string[] options = { "Carré", "Rectangle", "Triangle" };
         AfficherMenu(options);
         int choix = LireChoix(options);
         Forme forme = null;
         switch (options[choix-1])
         {
            case "Carré":
               forme = new Carré();
               break;
            case "Rectangle":
               forme = new Rectangle();
               break;
            case "Triangle":
               forme = new Triangle();
               break;
         }
         return forme;
      }
      static int LireNbFormes()
      {
         AfficherTitreÉtape("Entrée du nombre de formes souhaité");
         return LireEntierPositif();
      }
      static Forme[] LireFormes(int nbFormes)
      {
         AfficherTitreÉtape("Entrée des formes souhaitées");
         Forme[] formes = new Forme[nbFormes];
         for (int i = 0; i < formes.Length; ++i)
         {
            Console.WriteLine("Forme #{0}", i + 1);
            formes[i] = LireForme();
         }
         return formes;
      }
      static void Main(string[] args)
      {
         int nbFormes = LireNbFormes();
         Forme[] formes = LireFormes(nbFormes);
         foreach (Forme f in formes)
            f.Dessiner();
      }
   }
}

J'ai utilisé un foreach pour les répétitives où l'indice n'était pas important. Le code serait plus élégant si nous éliminions la partie validation des entrées, mais bon, c'est jamais très joli ce genre de truc.

Notez que C# permet les sélectives sur les chaînes de caractères, comme le fait Java d'ailleurs, un détail dont je me suis servi ici. Notez aussi que l'initialisation à null de la variable forme dans la méthode LireForme() est obligatoire pour éviter que le compilateur ne se plaigne d'un risque à l'exécution, bien que ce risque soit évité en pratique par la validation faite dans la méthode LireEntierBorne().

Solution possible – C# et List

Une variante éliminant le recours à un tableau, encore en C#, serait :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace z10
{
   class Program
   {
      static void AfficherTitreÉtape(string titre)
      {
         const int LARGEUR_LIGNE = 70;
         int largeurCadreCentral = (LARGEUR_LIGNE - 2 - titre.Length) / 2;
         Console.WriteLine(new string('-', LARGEUR_LIGNE));
         Console.WriteLine("{0} {1} {0}", new string('-', largeurCadreCentral), titre); // perfectible
         Console.WriteLine(new string('-', LARGEUR_LIGNE));
      }
      static int LireEntierPositif()
      {
         int nbLu = -1;
         do
         {
            try
            {
               Console.Write("Entrez un entier strictement positif : ");
               nbLu = int.Parse(Console.ReadLine());
            }
            catch (FormatException)
            {
            }
         }
         while (nbLu <= 0);
         return nbLu;
      }
      //
      // Note: les bornes sont inclusives
      //
      static int LireEntierBorné(int minVal, int maxVal)
      {
         if (maxVal < minVal)
            throw new ArgumentException(string.Format("Méthode LireEntierBorné(), maxVal = {0} et minVal = {1}", maxVal, minVal));
         bool ok = false;
         int nbLu = 0;
         do
         {
            try
            {
               Console.Write("Entrez une valeur entière entre {0} et {1} inclusivement : ", minVal, maxVal);
               nbLu = int.Parse(Console.ReadLine());
               if (minVal <= nbLu && nbLu <= maxVal)
                  ok = true;
            }
            catch (FormatException)
            {
            }
         }
         while (!ok);
         return nbLu;
      }
      static void AfficherMenu(string[] options)
      {
         for (int i = 0; i < options.Length; ++i)
         {
            Console.WriteLine("{0} : {1}", i + 1, options[i]);
         }
      }
      static int LireChoix(string[] options)
      {
         return LireEntierBorné(1, options.Length);
      }
      abstract class Forme
      {
         public abstract void Dessiner();
      }
      class Carré : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Carré");
         }
      }
      class Rectangle : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Rectangle");
         }
      }
      class Triangle : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Triangle");
         }
      }
      static Forme LireForme()
      {
         string[] options = { "Carré", "Rectangle", "Triangle" };
         AfficherMenu(options);
         int choix = LireChoix(options);
         Forme forme = null;
         switch (options[choix-1])
         {
            case "Carré":
               forme = new Carré();
               break;
            case "Rectangle":
               forme = new Rectangle();
               break;
            case "Triangle":
               forme = new Triangle();
               break;
         }
         return forme;
      }
      static int LireNbFormes()
      {
         AfficherTitreÉtape("Entrée du nombre de formes souhaité");
         return LireEntierPositif();
      }
      static List<Forme> LireFormes(int nbFormes)
      {
         AfficherTitreÉtape("Entrée des formes souhaitées");
         List<Forme> formes = new List<Forme>();
         for (int i = 0; i < nbFormes; ++i)
         {
            Console.WriteLine("Forme #{0}", i + 1);
            formes.Add(LireForme());
         }
         return formes;
      }
      static void Main(string[] args)
      {
         int nbFormes = LireNbFormes();
         List<Forme> formes = LireFormes(nbFormes);
         foreach (Forme f in formes)
            f.Dessiner();
      }
   }
}

Le type générique List<T> implémente un tableau dynamique de T, même si ça peut ne pas être évident en se basant sur son nom. Ce n'est vraiment pas une liste chaînée! Si vous n'êtes pas convaincus, écrivez un programme déclarant une vulgaire List<int> et ajoutez une vaste quantité d'éléments à la fin, puis faites de même en ajoutant au début. Ça devrait rendre évident à vos yeux les choix d'implémentation qui se cachent derrière ce type.

Solution possible – C++ avec vector

Une solution possible en C++ serait :

#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <algorithm>
using namespace std;

void afficher_titre_etape(string titre)
{
   const int LARGEUR_LIGNE = 70;
   const int largeurCadreCentral = (LARGEUR_LIGNE - 2 - titre.size()) / 2;
   const string DEMI_CADRE = string(largeurCadreCentral, '-');
   cout << string(LARGEUR_LIGNE, '-') << endl;
   cout << DEMI_CADRE << ' ' << titre << ' ' << DEMI_CADRE << endl; // perfectible
   cout << string(LARGEUR_LIGNE, '-') << endl;
}

int lire_entier_positif()
{
   int nbLu = -1;
   do
   {
      cout << "Entrez un entier strictement positif : ";
      if (!(cin >> nbLu))
      {
         cin.clear();
         cin.ignore();
      }
   }
   while (nbLu <= 0);
   return nbLu;
}

//
// Note: les bornes sont inclusives
//
class BornesIncorrectes {};
int lire_entier_borne(int minVal, int maxVal)
{
   if (maxVal < minVal)
      throw BornesIncorrectes();
   int nbLu = 0;
   bool ok = false;
   do
   {
      cout << "Entrez une valeur entière entre " << minVal << " et " << maxVal << " inclusivement : ";
      if (!(cin >> nbLu))
      {
         cin.clear();
         cin.ignore();
      }
      else if (minVal <= nbLu && nbLu <= maxVal)
         ok = true;
   }
   while (!ok);
   return nbLu;
}

void afficher_menu(vector<string> options)
{
   for (int i = 0; i < options.size(); ++i)
   {
      cout << i + 1 << " : " << options[i] << endl;
   }
}

int lire_choix(vector<string> options)
{
   return lire_entier_borne(1, options.size());
}

class Forme
{
public:
   virtual void dessiner() const = 0;
};

class Carre : public Forme
{
public:
   void dessiner() const override
   {
      cout << "Je suis un Carre" << endl;
   }
};

class Rectangle : public Forme
{
public:
   void dessiner() const override
   {
      cout << "Je suis un Rectangle" << endl;
   }
};

class Triangle : public Forme
{
public:
   void dessiner() const override
   {
      cout << "Je suis un Triangle" << endl;
   }
};

unique_ptr<Forme> lire_forme()
{
   const int CHOIX_CARRE = 0,
             CHOIX_RECTANGLE = 1,
             CHOIX_TRIANGLE = 2;
   string opts[] = { "Carre", "Rectangle", "Triangle" };
   vector<string> options (begin(opts), end(opts));
   // vector<string> options { "Carre", "Rectangle", "Triangle" };
   afficher_menu(options);
   int choix = lire_choix(options);
   unique_ptr<Forme> forme;
   switch(choix - 1)
   {
   case CHOIX_CARRE:
      forme = unique_ptr<Forme>(new Carre);
      break;
   case CHOIX_RECTANGLE:
      forme = unique_ptr<Forme>(new Rectangle);
      break;
   case CHOIX_TRIANGLE:
      forme = unique_ptr<Forme>(new Triangle);
      break;
   }
   return forme;
}

int lire_nb_formes()
{
   afficher_titre_etape("Entrée du nombre de formes souhaite");
   return lire_entier_positif();
}

vector<unique_ptr<Forme>> lire_formes(int nb_formes)
{
   afficher_titre_etape("Entree des formes souhaitees");
   vector<unique_ptr<Forme>> formes;
   for (int i = 0; i < nb_formes; ++i)
   {
      cout << "Forme #" << i + 1 << endl;
      formes.push_back(lire_forme());
   }
   return formes;
}

int main()
{
   int nb_formes = lire_nb_formes();
   auto formes = lire_formes(nb_formes);
   for(auto &f : formes)
      f->dessiner();
}

Il y a quelques irritants dans cette implémentation :

void afficher_menu(vector<string> options)
{
   for (int i = 0; i < options.size(); ++i)
   {
      cout << i + 1 << " : " << options[i] << endl;
   }
}
void afficher_menu(const vector<string> &options)
{
   for (vector<string>::size_type i = 0; i < options.size(); ++i)
   {
      cout << i + 1 << " : " << options[i] << endl;
   }
}

Comparatif sous forme de tableau

J'ai documenté le tout pour expliquer aux gens qui seraient moins familiers avec les idiomes contemporains de C++ certaines pratiques, mais cette documentation peut laisser l'impression d'une grande différence de complexité entre C++ et C#. Regardons de plus près le tout à partir d'un tableau comparatif :

Version C# Version C++ Remarques
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace z10
{
   class Program
   {
#include <vector>
#include <string>
#include <iostream>
#include <memory>
#include <algorithm>
using namespace std;

Pour la version C#, j'ai laissé sur place les using que Visual Studio insère par défaut, même si la plupart sont superflus dans ce cas-ci.

Pour la version C++, j'ai inséré les directives #include requises, mais je n'ai pas pris soin de faire des using individuels comme je le ferais dans mon propre code, privilégiant un using global sur std. Je pense que cela reflète ce qu'on demande à nos étudiant(e)s de S2.

// ...
      static void AfficherTitreÉtape(string titre)
      {
         const int LARGEUR_LIGNE = 70;
         int largeurCadreCentral = (LARGEUR_LIGNE - 2 - titre.Length) / 2;
         Console.WriteLine(new string('-', LARGEUR_LIGNE));
         Console.WriteLine("{0} {1} {0}", new string('-', largeurCadreCentral), titre);
         Console.WriteLine(new string('-', LARGEUR_LIGNE));
      }
// ...
void afficher_titre_etape(string titre)
{
   const int LARGEUR_LIGNE = 70;
   const int largeurCadreCentral = (LARGEUR_LIGNE - 2 - titre.size()) / 2;
   const string DEMI_CADRE = string(largeurCadreCentral, '-');
   cout << string(LARGEUR_LIGNE, '-') << endl;
   cout << DEMI_CADRE << ' ' << titre << ' ' << DEMI_CADRE << endl;
   cout << string(LARGEUR_LIGNE, '-') << endl;
}

Je pense que les différences principales entre ces deux implémentations est que C# force à utiliser new pour les string construites avec deux paramètres, et que C++ permet d'avoir des constantes dynamiques en plus des constantes statiques.

J'ai utilisé des minuscules pour la constante dynamique; ce choix esthétique peut être débattu. Le reste me semble cosmétique, sans plus.

// ...
      static int LireEntierPositif()
      {
         int nbLu = -1;
         do
         {
            try
            {
               Console.Write("Entrez un entier strictement positif : ");
               nbLu = int.Parse(Console.ReadLine());
            }
            catch (FormatException)
            {
            }
         }
         while (nbLu <= 0);
         return nbLu;
      }
// ...
int lire_entier_positif()
{
   int nbLu = -1;
   do
   {
      cout << "Entrez un entier strictement positif : ";
      if (!(cin >> nbLu))
      {
         cin.clear();
         cin.ignore();
      }
   }
   while (nbLu <= 0);
   return nbLu;
}

Tel que noté dans des questions antérieures, les entrées/ sorties de C++ ne lèvent pas d'exceptions; toutefois, un flux en erreur doit être nettoyé. Les exceptions constituent un thème essentiel, bien sûr, et il faut en venir à les enseigner éventuellement. J'en utilise d'ailleurs une dans le code C++ plus bas.

// ...
      //
      // Note: les bornes sont inclusives
      //
      static int LireEntierBorné(int minVal, int maxVal)
      {
         if (maxVal < minVal)
            throw new ArgumentException(
               string.Format(
                  "Méthode LireEntierBorné(), maxVal = {0} et minVal = {1}",
                  maxVal, minVal
               )
            );
         bool ok = false;
         int nbLu = 0;
         do
         {
            try
            {
               Console.Write(
                  "Entrez une valeur entière entre {0} et {1} inclusivement : ",
                  minVal, maxVal
               );
               nbLu = int.Parse(Console.ReadLine());
               if (minVal <= nbLu && nbLu <= maxVal)
                  ok = true;
            }
            catch (FormatException)
            {
            }
         }
         while (!ok);
         return nbLu;
      }
// ...
//
// Note: les bornes sont inclusives
//
class BornesIncorrectes {};
int lire_entier_borne(int minVal, int maxVal)
{
   if (maxVal < minVal)
      throw BornesIncorrectes();
   int nbLu = 0;
   bool ok = false;
   do
   {
      cout << "Entrez une valeur entière entre
           " << minVal << " et " << maxVal
           << " inclusivement : ";
      if (!(cin >> nbLu))
      {
         cin.clear();
         cin.ignore();
      }
      else if (minVal <= nbLu && nbLu <= maxVal)
         ok = true;
   }
   while (!ok);
   return nbLu;
}

Voir les remarques sur les entrées/ sorties ci-dessus.

En C#, les exceptions s'inscrivent dans une arborescence imposée (on tend à dériver nos exceptions de la classe ApplicationException), ce qui impose de survoler le concept d'héritage un peu rapidement, typiquement avant de pouvoir l'aborder avec sérieux. J'ai vécu cette situation au SIM; ça se gère mais ça entraîne un peu de confusion (mineure) pendant quelques semaines, le temps qu'on puisse approfondir le tout. Ici, je contourne le problème avec un ArgumentException général, mais c'est une mauvaise pratique.

Petite remarque : le code que j'ai écrit en C# est peu recommandable. En effet, il formate une chaîne créée dynamiquement dans une levée d'exception, ce qui pourrait mener à une exception Out of Memory. Une exception pendant un traitement d'exception. Très vilain.

En C++, on tend à utiliser des classes dont le type représente le problème souligné. Dans mon code, ces classes sont souvent vides, le type suffisant à détailler le problème, mais il est bien sûr possible de leur intégrer attributs, constructeurs, méthodes et ainsi de suite. Pas besoin d'utiliser new (en fait, en C++, on utilise très peu souvent new de manière directe; en pratique, c'est presque disparu du code client, mais j'y reviendrai).

// ...
      static void AfficherMenu(string[] options)
      {
         for (int i = 0; i < options.Length; ++i)
         {
            Console.WriteLine("{0} : {1}", i + 1, options[i]);
         }
      }
// ...
void afficher_menu(vector<string> options)
{
   for (int i = 0; i < options.size(); ++i)
   {
      cout << i + 1 << " : " << options[i] << endl;
   }
}

Ici, tel qu'indiqué plus haut, j'ai accepté un avertissement en C++ pour afficher l'indice de chaque option en plus de sa valeur, mais c'est pas gentil. Du code correct utiliserait le bon type pour l'indice, mais on pourrait débattre qu'il s'agirait d'une écriture compliquée en S2. Personnellement, je la montrerais, puisque c'est un meilleur design que de supposer int partout et que c'est une pratique très homogène, mais je comprendrais que les opinions varient sur le sujet.

Quand on n'a pas choisi d'afficher les indices, le code devient beaucoup plus simple, évidemment :

for(auto &s : options)
{
   cout << s << endl;
}

Si on souhaite la simplicité avant la vitesse, on peut même enlever le & avant la variable s et tout fonctionne sans problème.

// ...
      static int LireChoix(string[] options)
      {
         return LireEntierBorné(1, options.Length);
      }
// ...
int lire_choix(vector<string> options)
{
   return lire_entier_borne(1, options.size());
}

Peu de choses à dire ici, du point de vue d'un cours de S2; personnellement, j'enseignerais le passage des objets const & si je donnais le cours avec C++, quitte à en faire une recette, mais c'est une question d'hygiène plus qu'une obligation conceptuelle. Si nous écrivions du code multiprogrammé, toutefois, le code C# serait dangereux et il faudrait discuter des aléas du partage (implicite en C#) du tableau.

// ...
      abstract class Forme
      {
         public abstract void Dessiner();
      }
// ...
class Forme
{
public:
   virtual void dessiner() const = 0;
};

À mon avis, les distinctions entre les deux versions sont cosmétiques. Je suis de ceux qui pensent que abstract est un choix plus élégant que virtual ... =0 pour une méthode abstraite, soit dit en passant, même si je comprends les choix de part et d'autre.

Notez aussi que C++ permet d'indiquer au compilateur qu'une méthode d'instance est const, donc qu'elle ne modifie pas l'objet qui en est propriétaire; cette considération d'hygiène est absente de C# (snif!). La possibilité de qualifier une méthode const est l'un des trucs les plus douloureux pour moi quand j'utilise C#, mais il est clair que l'utiliser en C++ demande aussi de l'expliquer, donc ceci demande un effort d'enseignement et d'apprentissage supplémentaire.

// ...
      class Carré : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Carré");
         }
      }
      class Rectangle : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Rectangle");
         }
      }
      class Triangle : Forme
      {
         public override void Dessiner()
         {
            Console.WriteLine("Je suis un Triangle");
         }
      }
// ...
class Carre : public Forme
{
public:
   void dessiner() const override
   {
      cout << "Je suis un Carre" << endl;
   }
};
class Rectangle : public Forme
{
public:
   void dessiner() const override
   {
      cout << "Je suis un Rectangle" << endl;
   }
};
class Triangle : public Forme
{
public:
   void dessiner() const override
   {
      cout << "Je suis un Triangle" << endl;
   }
};

Évidemment, const fait partie de la signature de la méthode dessiner(), ce qui se répercute sur les enfants. Outre cela, ici encore, les implémentations sont semblables, sauf  peut-être pour le fait que C++ supporte d'autres formes d'héritage que l'héritage public, et que l'héritage privé y est le mode par défaut (ce qui explique le : public Forme). Sais pas pour vous, mais dans mon expérience, en S2, on voit plus ça comme une recette (en S4, on peut aller plus en profondeur sur des questions de design comme celle-là, du moins si le langage le permet).

J'ai mis le mot-clé contextuel override en C++ aussi, puisqu'il est supporté depuis C++ 11 (final aussi, dans le même sens qu'en Java et au sens de sealed en C#). Plusieurs sont contents; moi, override me laisse un peu froid (sealed et final, par contre, je suis plutôt content), mais je m'attends à ce que ce soit utilisé alors c'est sage de le montrer peu importe le langage.

// ...
      static Forme LireForme()
      {
         string[] options = { "Carré", "Rectangle", "Triangle" };
         AfficherMenu(options);
         int choix = LireChoix(options);
         Forme forme = null;
         switch (options[choix-1])
         {
            case "Carré":
               forme = new Carré();
               break;
            case "Rectangle":
               forme = new Rectangle();
               break;
            case "Triangle":
               forme = new Triangle();
               break;
         }
         return forme;
      }
// ...
unique_ptr<Forme> lire_forme()
{
   const int CHOIX_CARRE = 0,
             CHOIX_RECTANGLE = 1,
             CHOIX_TRIANGLE = 2;
   string opts[] = { "Carre", "Rectangle", "Triangle" };
   vector<string> options (begin(opts), end(opts));
   // vector<string> options { "Carre", "Rectangle", "Triangle" };
   afficher_menu(options);
   int choix = lire_choix(options);
   unique_ptr<Forme> forme;
   switch(choix - 1)
   {
   case CHOIX_CARRE:
      forme = unique_ptr<Forme>(new Carre);
      break;
   case CHOIX_RECTANGLE:
      forme = unique_ptr<Forme>(new Rectangle);
      break;
   case CHOIX_TRIANGLE:
      forme = unique_ptr<Forme>(new Triangle);
      break;
   }
   return forme;
}

Ici, on a un cas intéressant à mon avis :

  • Les langages C# et Java supportent les sélectives sur des string (des String avec Java). Ce n'est pas le cas de C++, donc ce code utilise des constantes. C'est plus ou moins joli (de toute manière, les sélectives, c'est pas terrible règle générale) et en pratique, je travaillerais autrement (voir plus bas), mais pas en S2
  • Faut bien sûr faire attention à la casse, mais dans le code en exemple ici, ce n' est pas un problème
  • Parce que j'ai utilisé un vector<string> en C++ (c'est la chose à faire), l'implémentation est plutôt simple dans l'ensemble, mais il y a une tache au dossier : mon compilateur (Visual Studio 2012 avec CTP de novembre 2012) ne supporte pas encore l'initialisation unifiée, qui permet d'initialiser n'importe quel type avec des valeurs entre accolades. Le code correct pour initialiser options est en commentaires dans cette proposition, et le passage par un tableau temporaire est une rustine (Patch) en attendant que le compilateur soit à jour sur ce point
  • J'ai utilisé un unique_ptr<Forme> comme type de retour pour la fonction en C++. C'est la chose à faire. On n'utilise plus de pointeurs bruts dans ce langage, sauf pour des applications spécialisées (en industrielle S3 ou S4, en gestion S4 ou S5)

Notez que le code C++ proposé ici est correct en C++ 11 mais en C++ 14, on remplacera ceci :

forme = unique_ptr<Forme>(new Carre);

... par cela :

forme = make_unique<Carre>();

Ça devrait déjà être ainsi, d'ailleurs, mais ça a échappé au comité de standardisation. La plupart des gens (dont moi) ont déjà codé un make_unique() en attendant, et std::make_unique() est déjà accepté pour C++ 14.

// ...
      static int LireNbFormes()
      {
         AfficherTitreÉtape("Entrée du nombre de formes souhaité");
         return LireEntierPositif();
      }
// ...
int lire_nb_formes()
{
   afficher_titre_etape("Entrée du nombre de formes souhaite");
   return lire_entier_positif();
}

Les différences entre les implémentations ici me semblent cosmétiques.

// ...
      static List<Forme> LireFormes(int nbFormes)
      {
         AfficherTitreÉtape("Entrée des formes souhaitées");
         List<Forme> formes = new List<Forme>();
         for (int i = 0; i < nbFormes; ++i)
         {
            Console.WriteLine("Forme #{0}", i + 1);
            formes.Add(LireForme());
         }
         return formes;
      }
// ...
vector<unique_ptr<Forme>> lire_formes(int nb_formes)
{
   afficher_titre_etape("Entree des formes souhaitees");
   vector<unique_ptr<Forme>> formes;
   for (int i = 0; i < nb_formes; ++i)
   {
      cout << "Forme #" << i + 1 << endl;
      formes.push_back(lire_forme());
   }
   return formes;
}

Les différences entre les implémentations me semblent cosmétiques ici aussi. C# alloue les objets dynamiquement, C++ le fait automatiquement, et C++ demande une réflexion à savoir si un objet alloué dynamiquement sera partagé ou non (par défaut, ne pas partager est plus sécuritaire, évidemment) alors on y privilégie unique_ptr (il existe aussi shared_ptr lorsque ça semble plus à propos). En C#, partager est la seule avenue; c'est plus simple mais plus dangereux dès qu'un programme devient multiprogrammé.

// ...
      static void Main(string[] args)
      {
         int nbFormes = LireNbFormes();
         List<Forme> formes = LireFormes(nbFormes);
         foreach (Forme f in formes)
            f.Dessiner();
      }
   }
}
// ...
int main()
{
   int nb_formes = lire_nb_formes();
   auto formes = lire_formes(nb_formes);
   for(auto &f : formes)
      f->dessiner();
}

Enfin, les différences ici sont encore une fois de l'ordre du cosmétique.

Notez que j'ai utilisé auto en C++ comme type de formes pour éviter d'écrire vector<unique_ptr<Forme>> alors que j'ai écrit List<Forme> en C#, mais en C# on aurait aussi pu écrire var pour obtenir le même effet. Je n'étais pas certain si c'était autant dans les moeurs, mais c'est probablement la chose à faire.

Détail syntaxique : en C++, les membres des objets accédés directement le sont par l'opérateur . alors que ceux accédés indirectement le sont par l'opérateur ->, alors qu'en C# ou en Java il n'y a pas d'accès direct aux objets, ce qui impose le recours à new un peu partout mais permet de se limiter à l'opérateur . pour accéder aux membres d'un référé.

Si nous souhaitons discuter avec des étudiantes et des étudiants un peu plus avancés, alors il devient possible d'aborder des solutions un peu plus élégantes. En particulier, nous pourrions combiner des tableaux associatifs et des fabriques pour éliminer les sélectives.

Solution possible – C# avec dictionnaire de fabriques

Ainsi, en C#, nous pourrions écrire :

// ...
      static Forme LireForme()
      {
         Dictionary<string, Func<Forme>> fabriques = new Dictionary<string,Func<Forme>>();
         fabriques.Add("Carré", ()=> new Carré());
         fabriques.Add("Rectangle", ()=> new Rectangle());
         fabriques.Add("Triangle", ()=> new Triangle());
         string[] options = { "Carré", "Rectangle", "Triangle" };
         AfficherMenu(options);
         int choix = LireChoix(options);
         return fabriques[options[choix - 1]]();
      }
// ...

Dans LireForme(), nous associons un nom de Forme (ceux dans le tableau de choix de menu) avec une fonction de fabrication (une Func<Forme>), et nous créons ces fonctions de fabrication à l'aide de λ-expressions, comme il se doit. Pour un aperçu de la syntaxe des λ en C#, voir ceci. Tristement, la correspondance entre le type retourné par la λ et le type du dictionnaire doit être directe en C#, ce qui force un transtypage (toujours une idée discutable). Il n'y a pas de raison pour ne pas accepter de λ-expressions qui retournent des enfants de Forme dans ce cas-ci; ça ressemble nettement à un bogue de spécification langagière.

Solution possible – C++ avec dictionnaire de fabriques

De même, en C++, nous pourrions écrire :

// ...
#include <functional>
#include <map>
// ...
unique_ptr<Forme> lire_forme()
{
   map<string, function<unique_ptr<Forme>()>> fabriques;
   fabriques["Carre"] = []() { return unique_ptr<Forme>(new Carre); };
   fabriques["Triangle"] = []() { return unique_ptr<Forme>(new Triangle); };
   fabriques["Rectangle"] = []() { return unique_ptr<Forme>(new Rectangle); };
   string opts[] = { "Carre", "Rectangle", "Triangle" };
   vector<string> options (begin(opts), end(opts));
   // vector<string> options { "Carre", "Rectangle", "Triangle" };
   afficher_menu(options);
   int choix = lire_choix(options);
   return fabriques[options[choix-1]]();
}
// ...

La std::map joue en C++ le même rôle que le Dictionary de .NET. Les différences sont cosmétiques. Pour comprendre la syntaxe des expressions λ en C++, voir cet article. On n'a pas besoin de transtyper le type de retour de nos fonctions de fabrication dans ce cas-ci puisqu'un enfant (p. ex. : Carre) est un cas particulier de son parent (Forme).


Valid XHTML 1.0 Transitional

CSS Valide !