Quelques raccourcis :

Université de Sherbrooke, développement du jeu vidéo, COA

Vous trouverez ici quelques documents et quelques liens pouvant, je l'espère, vous être utiles.

Les documents qui vous sont fournis ici le sont pour vous rendre service.

Je travaille très fort sur chacun de mes cours. Veuillez ne pas vendre (ou donner) les documents que je vous offre ici à qui que ce soit sans mon consentement. Si des abus surviennent, je vais cesser de rendre ce matériel disponible à toutes et à tous.

Si ces documents vous rendent service, faites-le moi savoir. Mon adresse de courriel est disponible via la page où on trouve mon horaire.

Si vous êtes curieuses ou curieux de voir un vieil examen (celui de la cohorte 02!), pour voir le style de question que vous pouvez rencontrer, voir UdeS-COA--Controle-Final--Cohorte-02.pdf (mais ne vous basez pas sur les questions pour le contenu, qui évolue à chaque année)

Plan de cours

Document d'accompagnement au plan de cours

À propos des séances en classe

Les quelques sections qui suivent réfèrent à ce que nous avons fait (ou allons faire) en classe cette session.

Séance

Date

Détail

S00

Mercredi 2 sept. 13 h 30-16 h 30

Au menu :

En vue de notre prochaine rencontre, je vous ai proposé de vous amuser avec le problème de la rédaction d'un jeu de Mastermind.

S01

Mercredi 9 sept. 13 h 30-16 h 30

Au menu :

Si vous souhaitez faire des exercices, dans les notes de cours :

  • Les templates sont discutés une première fois dans POO – Volume 02, pp. 11-31
  • Une brève introduction à la bibliothèque standard est proposée dans POO – Volume 02, pp. 50-95
  • Une introduction aux foncteurs est proposée dans POO – Volume 02, pp. 152-173
  • Une introduction aux λ est proposée dans POO – Volume 02, pp. 180-193
  • Une introduction aux singletons est proposée dans POO – Volume 02, pp. 222-241
  • La génération de nombres pseudoaélatoires est proposée dans POO – Volume 02, pp. 116-119

Une variante du code du singleton simpliste utilisé aujourd'hui va comme suit (voir https://wandbox.org/permlink/5HRCnprt4nhQDQEl pour une version exécutable) :

#include <iostream>
#include <random>
using namespace std;

class GenStochastique {
   mt19937 prng;
   GenStochastique() : prng{ random_device{}() } {
   }
public:
   using value_type = int;
   GenStochastique(const GenStochastique &) = delete;
   GenStochastique& operator=(const GenStochastique &) = delete;
   static GenStochastique &get() {
      static GenStochastique singleton; // définition (singleton Meyers)
      return singleton;
   }
   value_type prochain() {
      return uniform_int_distribution<value_type>{ 1, 100 }(prng);
   }
   // précondition : min <= max
   value_type prochain(value_type min, value_type max) {
      return uniform_int_distribution<value_type>{ min, max }(prng);
   }
};

int main() {
   auto & gen = GenStochastique::get();
   for (int i = 0; i != 10; ++i)
      cout << gen.prochain(1,6) << ' ';
}

Et voilà!

Pour vous donner une idée de ce à quoi la matière d'aujourd'hui peut servir, imaginez ceci :

// ...
class Objet3D {
   // ...
public:
   virtual void draw(IDirect3DDevice9 *device) = 0;
   virtual ~Objet3D() = default;
   // ...
};
class Dessiner {
   IDirect3DDevice9 *dev;
public:
   Dessiner(IDirect3DDevice9 *dev) : dev{ dev } {
   }
   void operator()(const Object3D *p) const {
      p->draw(dev);
   }
};
#include <vector>
#include <algorithm>
int main() {
   using namespace std;
   vector<Object3D*> v;
   IDirect3DDevice9 *device = ...
   //
   // remplir v...
   //
   // Tout afficher avec un foncteur (exemple)
   //
   for_each(begin(v), end(v), Dessiner{ device });
   // Tout libérer avec une λ (exemple)
   for_each(begin(v), end(v), [](Objet3D* p) {
      delete p;
   });
   // ... dans un cas comme celui-ci, on peut faire plus simple encore ;)
   for(auto p : v)
      delete p;
}

Comme vous pouvez le voir, afficher un monde (ici : les objets 3D vers lesquels pointent les éléments du vecteur v) et le nettoyer (ici, on suppose que les pointés peuvent être supprimés par delete, mais il y a des alternatives) devient tout simple quand on sait s'exprimer à l'intérieur des idiomes de notre langage.

Autre exemple simple mais sympathique : supposons un jeu où il y a des monstres et où il faut périodiquement filtrer ceux qui sont morts. Supposons que Monstre soit à peu près comme suit :

class Monstre {
   // ...
public:
   bool est_mort() const;
   // ...
   virtual ~Monstre();
};

... donc que Monstre soit une classe polymorphique telle que ce sont principalement ses dérivés que nous utilisons en pratique. Ainsi, une fonction qui filtre les monstres morts d'un vector<Monstre*> pourrait être :

vector<Monstre*> filtrer_morts(vector<Monstre*> v) {
   // [begin(v),p) est une séquence de non-morts, [p,end(v)) est une séquence de morts
   auto p = partition(begin(v), end(v), [](const Monstre *p) { return !p->est_mort(); });
   for_each(p, end(v), [](const Monstre *p) { delete p; }); // destruction des morts
   v.erase(p, end(v)); // élimination des éléments détruits
   return v;
}

... ou plus simplement encore :

vector<Monstre*> filtrer_morts(vector<Monstre*> v) {
   auto p = partition(begin(v), end(v), [](const Monstre *p) { return !p->est_mort(); });
   for_each(p, end(v), [](const Monstre *p) { delete p; }); // destruction des morts
   return { begin(v), p }; // on retourne ceux qui restent
}

Essayez de programmer cette fonction sans recours aux algorithmes standards et aux λ. Vous constaterez sans doute que le problème n'est pas banal...

S02

Mercredi 16 sept. 13 h 30-16 h 30

Au menu :

La premier version (fort perfectible, mais opérationnelle) de notre programme de génération d'identifiants était ce qui suit (séparé en fichiers distincts pour vous rendre service) :

generateurid.h
#ifndef GENERATEUR_ID_H
#define GENERATEUR_ID_H

class banque_vide {};
class jamais_pris {};
class deja_rendu {};

struct IGenerateurId {
   using value_type = unsigned short;
   virtual value_type prendre() = 0;
   virtual void rendre(value_type) = 0;
   virtual ~IGenerateurId() = default;
};

enum class Sorte { sequentiel, recycleur, aleatoire };

class sorte_incorrecte {};

class FabriqueGenerateurs {
public:
   IGenerateurId* creer(Sorte);
};

#endif
generateurid.cpp
#include "generateurid.h"

using namespace std; // je suis dans un .cpp alors ça ne concerne que moi

#include <limits>
#include <vector>
#include <algorithm>
#include <random>
#include <utility>
using namespace std;

class generateur_sequentiel : public IGenerateurId {
   int prochain = 0;
   static_assert(sizeof(int) > sizeof(value_type)); // mieux vaut être prudent(e)
public:
   value_type prendre() override {
      if (prochain > numeric_limits<value_type>::max())
         throw banque_vide{};
      return static_cast<value_type>(prochain++);
   }
   void rendre(value_type id) {
      if (id >= prochain)
         throw jamais_pris{};
   }
};

class generateur_recycleur : public IGenerateurId {
   vector<value_type> recycles;
   int prochain = 0;
   static_assert(sizeof(int) > sizeof(value_type)); // mieux vaut être prudent(e)
public:
   value_type prendre() override {
      if (!recycles.empty()) {
         auto id = recycles.back();
         recycles.pop_back();
         return id;
      }
      if (prochain > numeric_limits<value_type>::max())
         throw banque_vide{};
      return static_cast<value_type>(prochain++);
   }
   void rendre(value_type id) {
      if (id >= prochain)
         throw jamais_pris{};
      if (find(begin(recycles), end(recycles), id) != end(recycles))
         throw deja_rendu{};
      recycles.push_back(id);
   }
};

class generateur_aleatoire : public IGenerateurId {
   friend class FabriqueGenerateurs;
   vector<value_type> disponibles;
   mt19937 prng{ random_device{}() };
   static vector<value_type> generer_valeurs() {
      vector<value_type> v;
      for (int i = 0; i <= numeric_limits<value_type>::max(); ++i)
         v.push_back(static_cast<value_type>(i));
      return v;
   }
   generateur_aleatoire() : disponibles{ generer_valeurs() } {
   }
   value_type prendre() override{
      if (disponibles.empty())
         throw banque_vide{};
      uniform_int_distribution<vector<value_type>::size_type> de{ 0, disponibles.size() - 1 };
      auto n = de(prng);
      swap(disponibles[n], disponibles.back());
      auto id = disponibles.back();
      disponibles.pop_back();
      return id;
   }
   void rendre(value_type id) override {
      if (find(begin(disponibles), end(disponibles), id) != end(disponibles))
         throw jamais_pris{}; // ou deja_rendu, au choix
      disponibles.push_back(id);
   }
};

IGenerateurId* FabriqueGenerateurs::creer(Sorte sorte) {
   switch (sorte) {
   case Sorte::sequentiel:
      return new generateur_sequentiel;
   case Sorte::recycleur:
      return new generateur_recycleur;
   case Sorte::aleatoire:
      return new generateur_aleatoire;
   }
   throw sorte_incorrecte{};
}
principal.cpp

#include "generateurid.h"
#include <iostream>
using namespace std;

int main() {
   FabriqueGenerateurs fab;
   // IGenerateurId* p = fab.creer(Sorte::sequentiel);
   // IGenerateurId* p = fab.creer(Sorte::recycleur);
   IGenerateurId* p = fab.creer(Sorte::aleatoire);
   vector<IGenerateurId::value_type> v;
   for (int i = 0; i != 5; ++i) {
      v.push_back(p->prendre());
      cout << v.back() << '\n';
   }
   for (IGenerateurId::value_type n : v)
      p->rendre(n);
   for (int i = 0; i != 10; ++i) {
      cout << p->prendre() << '\n';
   }
   delete p;
}

Si vous souhaitez faire des exercices, dans les notes de cours :

  • L'héritage privé est expliqué dans POO – Volume 01, pp. 55-66
  • Les λ-expressions sont décrites dans POO – Volume 02, pp. 180-193

Il y a un an, le code que nous avions écrit était le suivant (à titre informatif; c'est très semblable d'une année à l'autre 🙂) :

GenerateurId.h
#ifndef GENERATEUR_ID_H
#define GENERATEUR_ID_H

#include <memory>

class sequentiel_t {};
inline sequentiel_t sequentiel;
class recycleur_t {};
inline recycleur_t recycleur;
class aleatoire_t {};
inline aleatoire_t aleatoire;

class banque_vide {};
class jamais_donne {};
class deja_rendu {};

class GenerateurId {
public:
   struct Impl;
private:
   std::unique_ptr<Impl> p;
public:
   using value_type = unsigned short;
   GenerateurId(sequentiel_t);
   GenerateurId(recycleur_t);
   GenerateurId(aleatoire_t);
   ~GenerateurId();
   value_type prendre();
   void rendre(value_type);
};

#endif
principal.cpp
#include "GenerateurId.h"
#include <iostream>
using namespace std;
int main() {
   GenerateurId gen{ sequentiel };
   cout << gen.prendre() << '\n';
   cout << gen.prendre() << '\n';
   cout << gen.prendre() << '\n';
}
GenerateurId.cpp
#include <algorithm>
#include <vector>
#include <iterator>
#include <numeric>
#include <random>
#include <limits>
using namespace std;

struct GenerateurId::Impl {
   using value_type = GenerateurId::value_type;
   virtual value_type prendre() = 0;
   virtual void rendre(value_type) = 0;
   virtual ~Impl() = default;
};

class GenSequentiel : public GenerateurId::Impl {
   value_type cur{};
   value_type prendre() override {
      if (cur < numeric_limits<value_type>::max()) // à réfléchir... on ne retourne jamais le max :(
         return cur++;
      throw banque_vide{};
   }
   void rendre(value_type id) override {
      if (id >= cur) throw jamais_donne{};
   }
};

class GenRecycleur : public GenerateurId::Impl {
   value_type cur{};
   vector<value_type> recycles;
   value_type prendre() override {
      if (!recycles.empty()) {
         auto id = recycles.back();
         recycles.pop_back();
         return id;
      }
      if (cur < numeric_limits<value_type>::max()) // à réfléchir... on ne retourne jamais le max :(
         return cur++;
      throw banque_vide{};
   }
   void rendre(value_type id) override {
      if (id >= cur) throw jamais_donne{};
      if (any_of(begin(recycles), end(recycles), [&](auto x) { return x == id; }))
         throw deja_rendu{};
      recycles.push_back(id);
   }
};

class GenAleatoire : public GenerateurId::Impl {
   mt19937 prng{ random_device{}() };
   vector<value_type> disponibles;
public:
   GenAleatoire() : disponibles(numeric_limits<value_type>::max()) {
      iota(begin(disponibles), end(disponibles), 0); // 0, 1, 2, ... , max
   }
private:
   value_type prendre() override {
      if (disponibles.empty()) throw banque_vide{};
      uniform_int_distribution<> de{ 0, static_cast<int>(disponibles.size()) - 1 };
      auto n = de(prng);
      auto id = disponibles[n];
      disponibles.erase(begin(disponibles) + n);
      return id;
   }
   void rendre(value_type id) override {
      if (any_of(begin(disponibles), end(disponibles), [&](auto x) { return x == id; }))
         throw deja_rendu{};
      disponibles.push_back(id);
   }
};

GenerateurId::GenerateurId(sequentiel_t) : p{ new GenSequentiel } {
}

GenerateurId::GenerateurId(recycleur_t) : p{ new GenRecycleur } {
}

GenerateurId::GenerateurId(aleatoire_t) : p{ new GenAleatoire } {
}

GenerateurId::~GenerateurId() = default;

auto GenerateurId::prendre() -> value_type {
   return p->prendre();
}
void GenerateurId::rendre(value_type id) {
   p->rendre(id);
}

S03

Mercredi 23 sept. 13 h 30-16 h 30

À venir

Le code d'aujourd'hui, en fin de séance, était ce qui suit (séparé en fichiers distincts pour vous rendre service) :

generateurid.h
#ifndef GENERATEUR_ID_H
#define GENERATEUR_ID_H

#include <memory>

class banque_vide {};
class non_attribue {};
class deja_rendu {};
//enum class SorteGenerateur { sequentiel, recycleur, aleatoire };
//class sorte_inconnue {};
class sorte_sequentielle_ {};
constexpr sorte_sequentielle_ sorte_sequentielle;
class sorte_recycleuse_ {};
constexpr sorte_recycleuse_ sorte_recycleuse;
class sorte_aleatoire_ {};
constexpr sorte_aleatoire_ sorte_aleatoire;

class GenerateurId {
public:
   struct Impl;
private:
   std::unique_ptr<Impl> p;
public:
   using value_type = unsigned short; // bof; std::uint16_t de <cstdint> serait mieux
   // GenerateurId(SorteGenerateur);
   GenerateurId(sorte_sequentielle_);
   GenerateurId(sorte_recycleuse_);
   GenerateurId(sorte_aleatoire_);
   ~GenerateurId();
   value_type prendre();
   void rendre(value_type);
};

#endif
generateurid.cpp
#include "IGenerateurId.h"

using namespace std; // je suis dans un .cpp alors ça ne concerne que moi

const GenerateurId::value_type id_min = 0, id_max = 65'535;

struct GenerateurId::Impl {
   using value_type = GenerateurId::value_type;
   virtual value_type prendre() = 0;
   virtual void rendre(value_type) = 0;
   virtual ~Impl() = default;
};

class GenerateurSequentiel : public GenerateurId::Impl {
   int prochain = id_min; // fragile, mais on s'en reparle dans 2-3 semaines ;)
   value_type prendre() override {
      if (prochain > id_max) 
         throw banque_vide{};
      return prochain++;
   }
   void rendre(value_type id) override {
      if (id >= prochain)
         throw non_attribue{};
   }
};

#include <vector>
#include <algorithm>
#include <numeric>

class GenerateurRecycleur : public GenerateurId::Impl {
   vector<value_type> recycles;
   int prochain = id_min; // fragile, mais on s'en reparle dans 2-3 semaines ;)
   value_type prendre() override {
      if (!recycles.empty()) {
         auto id = recycles.back();
         recycles.pop_back();
         return id;
      }
      if (prochain > id_max) 
         throw banque_vide{};
      return prochain++;
   }
   void rendre(value_type id) override {
      if (id >= prochain)
         throw non_attribue{};
      if (find(begin(recycles), end(recycles), id) != end(recycles))
         throw deja_rendu{};
      recycles.push_back(id);
   }
};

#include <random>

class GenerateurAleatoire : public GenerateurId::Impl {
   mt19937 prng{ random_device{}() };
   vector<value_type> disponibles;
   value_type prendre() override {
      if (disponibles.empty())
         throw banque_vide{};
      auto indice = uniform_int_distribution<>( 0, disponibles.size() - 1 )(prng);
      // note : ce qui suit est perfectible
      // auto id = disponibles[indice];
      // disponibles.erase(begin(disponibles) + indice);
      // note :e ce qui suit est mieux :)
      auto id = disponibles[indice];
      std::swap(disponibles[indice], disponibles.back());
      disponibles.pop_back();
      return id;
   }
   void rendre(value_type id) override {
      if (find(begin(disponibles), end(disponibles), id) != end(disponibles))
         throw non_attribue{};
      disponibles.push_back(id);
   }
   GenerateurAleatoire() : disponibles(static_cast<int>(id_max) - id_min + 1) {
      iota(begin(disponibles), end(disponibles), id_min);
   }
   friend class GenerateurId;
};

GenerateurId::GenerateurId(sorte_sequentielle_)
   : p{ new GenerateurSequentiel } {
}
GenerateurId::GenerateurId(sorte_recycleuse_)
   : p{ new GenerateurRecycleur } {
}
GenerateurId::GenerateurId(sorte_aleatoire_)
   : p{ new GenerateurAleatoire } {
}

//GenerateurId::GenerateurId(SorteGenerateur sorte) {
//   using enum SorteGenerateur;
//   switch (sorte) {
//   case sequentiel:
//      p = new GenerateurSequentiel; break;
//   case recycleur:
//      p = new GenerateurRecycleur; break;
//   case aleatoire:
//      p = new GenerateurAleatoire; break;
//   default:
//      throw sorte_inconnue{};
//   }
//}
GenerateurId::~GenerateurId() = default;

auto GenerateurId::prendre() -> value_type {
   return p->prendre();
}

void GenerateurId::rendre(value_type id) {
   p->rendre(id);
}
principal.cpp

#include "GenerateurId.h"
#include <iostream>
using namespace std;

int main() {
   GenerateurId gen{ sorte_aleatoire };
   for (int i = 0; i != 10; ++i)
      cout << gen.prendre() << endl;

   //auto p = FabriquerGenerateur(SorteGenerateur::recycleur);
   //for (int i = 0; i != 10; ++i)
   //   cout << p->prendre() << endl;
   //for (int i = 5; i != 2; --i)
   //   p->rendre(i);
   //for (int i = 0; i != 10; ++i)
   //   cout << p->prendre() << endl;
   //delete p;
}

Le code « final » de notre programme de génération d'identifiants était ce qui suit (séparé en fichiers distincts pour vous rendre service) :

generateurid.h
#ifndef GENERATEUR_ID_H
#define GENERATEUR_ID_H

#include <memory>

class banque_vide {};
class non_attribue {};
class deja_rendu {};
//enum class SorteGenerateur { sequentiel, recycleur, aleatoire };
//class sorte_inconnue {};
class sorte_sequentielle_ {};
constexpr sorte_sequentielle_ sorte_sequentielle;
class sorte_recycleuse_ {};
constexpr sorte_recycleuse_ sorte_recycleuse;
class sorte_aleatoire_ {};
constexpr sorte_aleatoire_ sorte_aleatoire;

class GenerateurId {
public:
   struct Impl;
private:
   std::unique_ptr<Impl> p;
public:
   using value_type = unsigned short; // bof; std::uint16_t de <cstdint> serait mieux
   // GenerateurId(SorteGenerateur);
   GenerateurId(sorte_sequentielle_);
   GenerateurId(sorte_recycleuse_);
   GenerateurId(sorte_aleatoire_);
   ~GenerateurId();
   value_type prendre();
   void rendre(value_type);
};

#endif
generateurid.cpp
#include "IGenerateurId.h"

using namespace std; // je suis dans un .cpp alors ça ne concerne que moi

const GenerateurId::value_type id_min = 0, id_max = 65'535;

struct GenerateurId::Impl {
   using value_type = GenerateurId::value_type;
   virtual value_type prendre() = 0;
   virtual void rendre(value_type) = 0;
   virtual ~Impl() = default;
};

class GenerateurSequentiel : public GenerateurId::Impl {
   int prochain = id_min; // fragile, mais on s'en reparle dans 2-3 semaines ;)
   value_type prendre() override {
      if (prochain > id_max) 
         throw banque_vide{};
      return prochain++;
   }
   void rendre(value_type id) override {
      if (id >= prochain)
         throw non_attribue{};
   }
};

#include <vector>
#include <algorithm>
#include <numeric>

class GenerateurRecycleur : public GenerateurId::Impl {
   vector<value_type> recycles;
   int prochain = id_min; // fragile, mais on s'en reparle dans 2-3 semaines ;)
   value_type prendre() override {
      if (!recycles.empty()) {
         auto id = recycles.back();
         recycles.pop_back();
         return id;
      }
      if (prochain > id_max) 
         throw banque_vide{};
      return prochain++;
   }
   void rendre(value_type id) override {
      if (id >= prochain)
         throw non_attribue{};
      if (find(begin(recycles), end(recycles), id) != end(recycles))
         throw deja_rendu{};
      recycles.push_back(id);
   }
};

#include <random>

class GenerateurAleatoire : public GenerateurId::Impl {
   mt19937 prng{ random_device{}() };
   vector<value_type> disponibles;
   value_type prendre() override {
      if (disponibles.empty())
         throw banque_vide{};
      auto indice = uniform_int_distribution<>( 0, disponibles.size() - 1 )(prng);
      // note : ce qui suit est perfectible
      // auto id = disponibles[indice];
      // disponibles.erase(begin(disponibles) + indice);
      // note :e ce qui suit est mieux :)
      auto id = disponibles[indice];
      std::swap(disponibles[indice], disponibles.back());
      disponibles.pop_back();
      return id;
   }
   void rendre(value_type id) override {
      if (find(begin(disponibles), end(disponibles), id) != end(disponibles))
         throw non_attribue{};
      disponibles.push_back(id);
   }
   GenerateurAleatoire() : disponibles(static_cast<int>(id_max) - id_min + 1) {
      iota(begin(disponibles), end(disponibles), id_min);
   }
   friend class GenerateurId;
};

GenerateurId::GenerateurId(sorte_sequentielle_)
   : p{ new GenerateurSequentiel } {
}
GenerateurId::GenerateurId(sorte_recycleuse_)
   : p{ new GenerateurRecycleur } {
}
GenerateurId::GenerateurId(sorte_aleatoire_)
   : p{ new GenerateurAleatoire } {
}

//GenerateurId::GenerateurId(SorteGenerateur sorte) {
//   using enum SorteGenerateur;
//   switch (sorte) {
//   case sequentiel:
//      p = new GenerateurSequentiel; break;
//   case recycleur:
//      p = new GenerateurRecycleur; break;
//   case aleatoire:
//      p = new GenerateurAleatoire; break;
//   default:
//      throw sorte_inconnue{};
//   }
//}
GenerateurId::~GenerateurId() = default;

auto GenerateurId::prendre() -> value_type {
   return p->prendre();
}

void GenerateurId::rendre(value_type id) {
   p->rendre(id);
}
principal.cpp

#include "GenerateurId.h"
#include <iostream>
using namespace std;

int main() {
   GenerateurId gen{ sorte_aleatoire };
   for (int i = 0; i != 10; ++i)
      cout << gen.prendre() << endl;

   //auto p = FabriquerGenerateur(SorteGenerateur::recycleur);
   //for (int i = 0; i != 10; ++i)
   //   cout << p->prendre() << endl;
   //for (int i = 5; i != 2; --i)
   //   p->rendre(i);
   //for (int i = 0; i != 10; ++i)
   //   cout << p->prendre() << endl;
   //delete p;
}
  • Retour sur certains aspects de cette investigation :

Par la suite :

Si vous souhaitez faire des exercices, dans les notes de cours :

  • L'amitié (friend) est décrite dans POO – Volume 02, pp. 251-257
  • Une introduction aux objets opaques, aux interfaces et aux fabriques (avec un survol de l'idiome pImpl) est proposée dans POO – Volume 02, pp. 136-151
  • Les pointeurs intelligents sont discutés dans POO – Volume 02, pp. 120-135. Notez que c'est un sujet vaste sur lequel nous reviendrons à plusieurs reprises
  • L'essentiel de ce qui a été présenté aujourd'hui se trouve sur les liens susmentionnés. Toutefois, la semaine prochaine, le coeur de notre réflexion se trouvera dans le document POO – Volume 02, pp. 96-115 (voir en particulier la page 111 pour la sémantique de mouvement, qui risque de surprendre certains d'entre vous)

Pour un exemple d'utilisation de std::unique_ptr avec une fonction de nettoyage du pointé qui soit différente de l'opérateur delete (qui est le comportement par défaut), voici un petit exemple opérationnel. J'ai utilisé un truc qui expose un Release() bidon pour me coller un peu à ce que vous utilisez avec DirectX, mais sachez que ce qui se fait avec DirectX (et COM en général) est plus subtil que ce que laisse entendre cet exemple :

#include <iostream>
#include <memory>
using namespace std;
struct ID3DMachinChouette {
   virtual int fonction_tres_importante() = 0;
   virtual void Release() = 0;
   friend void relacher(ID3DMachinChouette *);
protected:
   virtual ~ID3DMachinChouette() = default;
};
class MachinChouette : public ID3DMachinChouette {
   //
   // Remarquez: toutes les méthodes sont privées. Pourtant,
   // ça fonctionne. Pourquoi?
   //
   int fonction_tres_importante() override {
      return 3; // calcul très savant, évidemment
   }
   void Release() override {
      delete this; // urg! Mais propre et légal
   }
};
void relacher(ID3DMachinChouette *p) {
   if (p) p->Release();
}
auto creer_machin_chouette() {
   return unique_ptr<ID3DMachinChouette, void(*)(ID3DMachinChouette*)>{
      new MachinChouette, relacher
   };
}
int main() {
   auto p = creer_machin_chouette();
   cout << p->fonction_tres_importante() << endl;
}

Le destructeur de p, qui est une instance du type unique_ptr<ID3DMachinChouette> et mène en fait vers un MachinChouette, est responsable de détruire le pointé, et le fait à l'aide d'un appel à relacher() tel que nous le lui avons demandé.

Notez que j'ai utilisé auto pour le type de p. Ceci signifie « p est du type de l'expression utilisée pour l'initialiser ». Dans ce cas, puisque p est initialisé par ce que retourne creer_machin_chouette(), alors son type est :

unique_ptr<ID3DMachinChouette, void(*)(ID3DMachinChouette*)>

Ce qui rend cette écriture lourde est le fait que nous utilisons une fonction comme outil de nettoyage, et que le type des pointeurs de fonctions n'est pas le plus élégant que nous ait légué le langage C. Une alternative utilisant un foncteur dont l'opérateur () serait générique nous donnerait l'écriture suivante (plus élégante, vous en conviendrez). Les modifications sont indiquées à même les commentaires :

#include <iostream>
#include <memory>
using namespace std;
struct ID3DMachinChouette {
   virtual int fonction_tres_importante() = 0;
   virtual void Release() = 0;
   friend struct Relacher; // <-- ICI
protected:
   virtual ~ID3DMachinChouette() = default;
};
class MachinChouette : public ID3DMachinChouette {
   //
   // Remarquez: toutes les méthodes sont privées. Pourtant,
   // ça fonctionne. Pourquoi?
   //
   int fonction_tres_importante() override {
      return 3; // calcul très savant, évidemment
   }
   void Release() override {
      delete this; // urg! Mais propre et légal
   }
};
struct Relacher { // <-- ICI (toute la classe)
   template <class T>
      void operator()(T p) const {
         p->Release();
      }
};
auto creer_machin_chouette() { // <-- ICI
   return unique_ptr<ID3DMachinChouette, Relacher>{ new MachinChouette }; // <-- ICI
}
int main() {
   auto p = creer_machin_chouette();
   cout << p->fonction_tres_importante() << endl;
}

Dans ce cas, même la construction du unique_ptr dans creer_machin_chouette() est plus légère, du fait que seule une instance de Relacher peut servir à titre de nettoyeur, et du fait que Relacher a un constructeur par défaut (tout à fait suffisant). En plus, on vient d'épargner un peu d'espace en mémoire en enchâssant Relacheur dans le type de l'objet plutôt que dans ses états.

Si l'écriture d'un foncteur comme Relacher ne vous sied pas, il est possible de fair basculer la fonction de destruction (le Custom Deleter) du monde des valeurs, à l'exécution, au monde des types, l'enchâssant dans le type du unique_ptr, à travers une expression λ et de l'opérateur decltype :

#include <iostream>
#include <memory>
using namespace std;
struct ID3DMachinChouette {
   virtual int fonction_tres_importante() = 0;
   virtual void Release() = 0;
   friend void relacher(ID3DMachinChouette *);
protected:
   virtual ~ID3DMachinChouette() = default;
};
class MachinChouette : public ID3DMachinChouette {
   //
   // Remarquez: toutes les méthodes sont privées. Pourtant,
   // ça fonctionne. Pourquoi?
   //
   int fonction_tres_importante() override {
      return 3; // calcul très savant, évidemment
   }
   void Release() override {
      delete this; // urg! Mais propre et légal
   }
};
void relacher(ID3DMachinChouette *p) {
   if (p) p->Release();
}
auto creer_machin_chouette() {
   return unique_ptr<ID3DMachinChouette, decltype([](ID3DMachinChouette *p) {
      relacher(p);
   })>{
      new MachinChouette
   };
}
int main() {
   auto p = creer_machin_chouette();
   cout << p->fonction_tres_importante() << endl;
}

Dans cet exemple, nous n'entreposons pas le pointeur de fonction dans le unique_ptr. Nous inscrivons dans son type le type d'un foncteur (la λ) qui appellera la fonction relacher quand on appellera son opérateur ().

s/o

Mercredi 30 sept.

Journée de la réconciliation (cours suspendus)

S04

Mercredi 7 oct. 13 h 30-16 h 30

Au menu : à venir

S05

Mercredi 14 oct. 13 h 30-16 h 30

Au menu : à venir

S06

Mercredi 21 oct. 13 h 30-16 h 30

Au menu : à venir

S07

Mercredi 28 oct. 13 h 30-16 h 30

Au menu : à venir

S08

Mercredi 4 nov. 13 h 30-16 h 30

Au menu : à venir

S09

Mardi 10 nov. 13 h 30-16 h 30

Au menu : à venir

S10

Mercredi 11 nov. 13 h 30-16 h 30

Au menu : à venir

S11

Mercredi 18 nov. 13 h 30-16 h 30

Au menu : à venir

S12

Mercredi 25 nov. 13 h 30-16 h 30

Au menu : à venir

S13

Mercredi 2 déc. 13 h 30-16 h 30

Au menu : à venir

S14

Mercredi 9 déc. 13 h 30-16 h 30

Chic examen plein d'amour

« S15 »

 

Semaine de finalisation et de présentation du projet, avec présentation (date et heure à venir)

Code des cas sous étude dans les notes de cours

Ce qui suit vous est gracieusement offert dans le but de vous épargner une recopie pénible d'exemples et de tests proposés dans les notes de cours.

Exercice à faire suite à la séance S00

L'exercice proposé est celui de la construction d'un petit jeu de devinette

Exercice à faire suite à la séance S01

L'exercice proposé est celui de la construction d'un petit jeu de Mastermind

Consignes des travaux pratiques

Les consignes des travaux pratiques en lien avec le cours sont ci-dessous.

Consignes propres aux deux travaux pratiques

De manière générale :

Consignes du TP00

Date de remise : début de la séance S06.

Ce travail s'intègre aux travaux pratiques de vos autres cours de la session, et est à faire sur une base individuelle. Réfléchissez à l'application pertinente et justifiée par vous-même dans l'un ou l'autre de ces travaux pratiques :

Si vous éprouvez des difficultés à intégrer un élément technique précis, je suis ouvert à ce que vous proposiez une thématique technique équivalente. Dans le doute sur le caractère équivalent ou non de votre proposition, contactez-moi!

Notez que ce que vous proposerez doit se distinguer des exemples proposés par votre chic prof. Présentez chaque application en la mettant en contexte et en décrivant son fonctionnement, code à l'appui. Montrez clairement les éléments que vous intégrez à vos projets, et mettez en valeur la pertinence de vos choix. Le but de ce travail est de vous aider à progresser dans votre développement, pas de vous nuire.

Exemple de format possible

Pour qu'on se comprenne bien, voici le genre de truc auquel je m'attends (adaptez à votre style personnel, évidemment), mais avec un exemple simpliste. Supposons que vous avez refactorisé une fonction écrite à l'ancienne pour profiter des algorithmes standards (notez que dans cet exemple, on a aussi une λ alors c'est un « deux pour un »!) :

Cher chic prof, dans notre projet, nous avons rapidement produit beaucoup de code pour s'apercevoir, en cours de route, qu'il était devenu difficile à entretenir. Je me suis penché sur quelques fonctions particulièrement « douloureuses » à mes yeux, et j'ai essayé de les retravailler pour que nous puissions mieux les comprendre et les faire évoluer.

En particulier, j'ai rencontré cette chose :

int trouver_plus_pres_de(Point pt, vector<Point> candidats) {
   if (candidats.size() == 0) return -1; // personne
   int reponse = 0; // optimiste
   double plus_proche_dist = numeric_limits<double>::max(); // loin!
   for(int i = 0; i < candidats.size(); ++i) {
      if (pt != candidats[i] && pt - candidats[i] < plus_proche_dist) {
         reponse = i;
         plus_proche_dist = pt - candidats[i];
      }
   }
   return reponse;
}

C'est pas très joli, j'en conviens. En y regardant de plus près, on y voit :

Je me suis demandé si je serais capable de mettre en mots ce que la fonction fait, comme tu l'as suggéré en classe, et je me suis dit que le nom de la fonction faisait bien ce travail : cette fonction sert à trouver le Point dans candidats qui est le plus proche de pt (sans y être superposé). Je me suis dit alors que je pourrais y aller comme ceci :

Si candidats est vide
    Retourner -1 // cas dénégéré
plus_proche ← trouver premier autre que pt dans candidats
Si plus_proche = pt
   Retourner 0 // seul au monde
distance_plus_proche ← *plus_proche - pt
Tant que je n'ai pas épuisé les Points candidats
   plus_proche ← trouver plus proche que plus_proche
   distance_plus_proche ← *plus_proche - pt
Retourner distance(debut(candidats), plus_proche)

Ce qui se traduit directement en code :

int trouver_plus_pres_de(const Point &pt, const vector<Point> &candidats) {
   if (candidats.empty()) return -1; // cas dégénéré
   auto plus_proche = find_if_not(next(begin(candidats)), end(candidats), pt);
   if (plus_proche == end(candidats)) return 0; // seul au monde
   auto distance_plus_proche = *plus_proche - pt;
   for(plus_proche = find_if(plus_proche, end(candidats), [&](const Point &p) { return p != pt && pt - p < distance_plus_proche; });
       plus_proche != end(candidats);
       plus_proche = find_if(plus_proche, end(candidats), [&](const Point &p) { return p != pt && pt - p < distance_plus_proche; }))
      distance_plus_proche = *plus_proche - pt
   return distance(begin(candidats), plus_proche);
}

C'était pas si mal, mais ce n'était pas encore le bonheur. Entre autres, il y avait un peu plus de code qu'avant, ce qui me rendait triste.

J'ai pris sur moi de retravailler ma solution. En nommant la λ, c'était déjà un peu mieux (mais fallait vraiment capturer par référence, sinon ça ne fonctionnait pas) :

int trouver_plus_pres_de(const Point &pt, const vector<Point> &candidats) {
   if (candidats.empty()) return -1; // cas dégénéré
   auto plus_proche = find_if_not(next(begin(candidats)), end(candidats), pt);
   if (plus_proche == end(candidats)) return 0; // seul au monde
   auto distance_plus_proche = *plus_proche - pt;
   auto pred = [&](const Point &p) { return p != pt && pt - p < distance_plus_proche; };
   for(plus_proche = find_if(plus_proche, end(candidats), pred);
       plus_proche != end(candidats);
       plus_proche = find_if(plus_proche, end(candidats), pred))
      distance_plus_proche = *plus_proche - pt
   return distance(begin(candidats), plus_proche);
}

Cependant, j'en ai discuté avec mes collègues et nous nous sommes aperçus que ce dont nous avions vraiment besoin, c'était la distance la plus courte entre un Point candidat et pt. Nous nous donnions la peine de repérer une position alors que nous ne voulions qu'un minimum! J'ai donc réécrit le tout plus simplement :

double plus_courte_distance_de(const Point &pt, const vector<Point> &candidats) {
   if (candidats.empty()) return -1.0; // cas dégénéré
   auto cur = find_if_not(begin(candidats), end(candidats), [&pt](const Point &p) { return p != pt; });
   if (cur == end(candidats)) return 0.0; // seul au monde
   return accumulate(next(cur), end(candidats), *cur - pt, [&pt](double so_far, const Point &p) {
      return pt == p? so_far : std::min(so_far, pt - p);
   });
}

... et c'est plus direct, plus clair et plus rapide (voir résultats des tests en annexe). Voilà!

Notez qu'il arrive régulièrement que des étudiant(e)s de ce cours arrivent au point où il leur faut livrer ce travail pratique et font le constat qu'ils n'ont pas encore commencé à appliquer des techniques et des concepts du cours, ayant (consciemment ou non) fait le « choix » de rester dans le confort de leurs habitudes. Ne vous faites pas prendre; prévoyez le coup en mettant le plus systématiquement possible en pratique ce que vous voyez dans le cours!

Consignes du TP01

Date de remise : début de la séance S12 (en fait, quand vous aurez fini votre projet, car c'est la priorité)

Il se fait tard, vous souhaitez produire et conclure votre session, et je souhaite pouvoir savoir où vous en êtes sans toutefois vous surcharger. Voici ce que je vous propose :

L'idée est de produire une très brève autocritique de contribution individuelle et de design global qui puisse servir de rampe de lancement au projet de cet hiver. Autocritique ne signifie pas autoflagellation : personne n'est parfait et l'école est un lieu d'apprentissage.


Valid XHTML 1.0 Transitional

CSS Valide !