Vous trouverez ici quelques documents et quelques liens pouvant, je l'espère,
vous être utiles.
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 27 août 8 h 30-11 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
|
Jeudi 4 sept. 13 h-16 h
|
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é ce matin 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 10 sept. 13 h-16 h
|
Au menu :
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;
}
Si vous souhaitez faire des exercices, dans les notes de cours :
- L'héritage privé est expliqué dans POO – Volume 01, pp. 55-66
- 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 λ-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 17 sept. 8 h 30-11 h 30
|
À venir
Si vous souhaitez faire des exercices, dans les notes de cours :
- L'amitié (friend) est décrite
dans POO
– Volume
02, pp. 251-257
- 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
().
|
S04
|
Mercredi 24 sept. 13 h-16 h
|
- Q02
- Retour sur Q01
- Déclarer ou définir les fonctions
- La question du code sécuritaire
- Quelques distractions instructives :
- Utiliser std::vector,
std::list ou std::deque?
- Quelques tests de vitesse :
0_0,
0_0b,
0_1,
1_0,
1_1,
1_2,
1_3,
2_0,
2_1,
2_2,
3_0
- Écrire une surcharge d'opérateurs selon la
syntaxe « méthode » et selon la syntaxe « fonction »
- Poursuite de la conception d'un tableau dynamique de int,
pour explorer diverses ramifications (pas toujours évidentes) de la
conception d'un tel conteneur
Pour celles et ceux qui le souhaitent, le Tableau
modélisant un (simpliste et encore incomplet) tableau dynamique d'entiers que nous avons écrit ressemblait à ceci :
#include <cstddef> // std::size_type
#include <algorithm>
#include <utility>
#include <ostream>
class Tableau {
public:
using value_type = int;
using size_type = std::size_t;
using pointer = value_type*;
using const_pointer = const value_type*;
using reference = value_type&;
using const_reference = const value_type&;
private:
pointer elems{};
size_type nelems{},
cap{};
public:
[[nodiscard]] size_type size() const noexcept {
return nelems;
}
[[nodiscard]] bool empty() const noexcept {
return !size();
}
[[nodiscard]] size_type capacity() const noexcept {
return cap;
}
private:
[[nodiscard]] bool full() const noexcept {
return size() == capacity();
}
public:
using iterator = pointer;
using const_iterator = const_pointer;
[[nodiscard]] iterator begin() noexcept {
return elems;
}
[[nodiscard]] const_iterator begin() const noexcept {
return elems;
}
[[nodiscard]] const_iterator cbegin() const noexcept {
return begin();
}
[[nodiscard]] iterator end() noexcept {
return begin() + size();
}
[[nodiscard]] const_iterator end() const noexcept {
return begin() + size();
}
[[nodiscard]] const_iterator cend() const noexcept {
return end();
}
Tableau() = default;
Tableau(size_type n, const_reference val)
: elems{ new value_type[n] }, nelems{ n }, cap{ n } {
std::fill(begin(), end(), val);
}
Tableau(const Tableau &autre)
: elems{ new value_type[autre.size()] }, nelems{ autre.size() }, cap{ autre.size() } {
std::copy(autre.begin(), autre.end(), begin());
}
Tableau(Tableau && autre) noexcept
: elems{ std::exchange(autre.elems, nullptr) },
nelems{ std::exchange(autre.nelems, 0) },
cap{ std::exchange(autre.cap, 0) } {
}
~Tableau() {
delete [] elems;
}
void swap(Tableau &autre) noexcept {
using std::swap;
swap(elems, autre.elems);
swap(nelems, autre.nelems);
swap(cap, autre.cap);
}
Tableau& operator=(const Tableau &autre) {
Tableau{ autre }.swap(*this);
return *this;
}
Tableau &operator=(Tableau &&autre) noexcept {
Tableau{ std::move(autre) }.swap(*this);
return *this;
}
[[nodiscard]] reference operator[](size_type n) noexcept {
return elems[n];
}
[[nodiscard]] const_reference operator[](size_type n) const noexcept {
return elems[n];
}
[[nodiscard]] bool operator==(const Tableau &autre) const noexcept {
return size() == autre.size() && std::equal(begin(), end(), autre.begin());
}
[[nodiscard]] bool operator!=(const Tableau &autre) const noexcept {
return !(*this == autre);
}
void push_back(const_reference val) {
if(full()) grow();
elems[size()] = val;
++nelems;
}
void pop_back() noexcept {
--nelems;
}
void clear() noexcept {
nelems = {};
}
[[nodiscard]] reference front() noexcept {
return (*this)[0];
}
[[nodiscard]] const_reference front() const noexcept {
return (*this)[0];
}
[[nodiscard]] reference back() noexcept {
return (*this)[size() - 1];
}
[[nodiscard]] const_reference back() const noexcept {
return (*this)[size() - 1];
}
private:
void grow() {
const auto nouv_cap = capacity()? static_cast<size_type>(capacity() * 1.5) : 42;
auto p = new value_type[nouv_cap];
std::copy(begin(), end(), p);
delete [] elems;
elems = p;
cap = nouv_cap;
}
};
std::ostream &operator<<(std::ostream &os, const Tableau &tab) {
for (auto n : tab)
os << n << ' ';
return os;
}
//
// code client naïf
//
#include <iostream>
#include <iterator>
using namespace std;
int main() {
Tableau t;
for(int i = 0; i != 100; ++i)
t.push_back(i + 1);
cout << "Apres 100 insertions, " << t.size() << " elems (capacite de " << t.capacity()
<< ")\nValeurs : " << t << endl;
if (auto p = find_if(begin(t), end(t), [](int n) { return n > 10 && n % 2 == 0; });
p != end(t))
cout << "\n\n" << *p << endl;
}
Pour le code du tableau générique tel que vu en classe, voici :
#include <cstddef>
#include <algorithm>
#include <initializer_list>
#include <utility>
template <class T>
class Tableau {
public:
using value_type = T;
using size_type = std::size_t;
using pointer = value_type*;
using const_pointer = const value_type*;
using reference = value_type&;
using const_reference = const value_type&;
private:
pointer elems {};
size_type nelems {},
cap {};
public:
[[nodiscard]] size_type size() const noexcept {
return nelems;
}
[[nodiscard]] size_type capacity() const noexcept {
return cap;
}
[[nodiscard]] bool empty() const noexcept {
return !size();
}
private:
[[nodiscard]] bool full() const noexcept {
return size() == capacity();
}
public:
using iterator = pointer;
using const_iterator = const_pointer;
[[nodiscard]] iterator begin() noexcept {
return elems;
}
[[nodiscard]] const_iterator begin() const noexcept {
return elems;
}
[[nodiscard]] const_iterator cbegin() const noexcept {
return begin();
}
[[nodiscard]] iterator end() noexcept {
return begin() + size();
}
[[nodiscard]] const_iterator end() const noexcept {
return begin() + size();
}
[[nodiscard]] const_iterator cend() const noexcept {
return end();
}
Tableau() = default;
Tableau(std::initializer_list<value_type> lst)
: elems{ new value_type[lst.size()] },
nelems{ lst.size() }, cap{ lst.size() } {
try {
std::copy(lst.begin(), lst.end(), begin());
} catch(...) {
delete[] elems;
throw;
}
}
//
// Notez que le constructeur ci-dessous peut bénéficier
// du recours à enable_if pour éviter certaines ambiguïtés
//
Tableau(size_type n, const_reference init)
: cap{ n }, nelems{ n }, elems{ new value_type[n] } {
try {
std::fill(begin(), end(), init);
} catch(...) {
delete[] elems;
throw;
}
}
Tableau(const Tableau &autre)
: elems{ new value_type[autre.size()] },
nelems{ autre.size() }, cap{ autre.size() } {
try {
std::copy(autre.begin(), autre.end(), begin());
} catch(...) {
delete[] elems;
throw;
}
}
//
// Notez que le constructeur ci-dessous peut bénéficier
// du recours à enable_if pour éviter certaines ambiguïtés
//
template <class It>
Tableau(It debut, It fin)
: nelems(std::distance(debut, fin)) {
cap = size();
elems = new value_type[size()];
try {
std::copy(debut, fin, begin());
} catch(...) {
delete[] elems;
throw;
}
}
template <class U>
Tableau(const Tableau<U> &autre)
: cap{ autre.size() }, nelems{ autre.size() },
elems{ new value_type[autre.size()] } {
try {
std::copy(autre.begin(), autre.end(), begin());
} catch(...) {
delete[] elems;
throw;
}
}
template <class U>
Tableau& operator=(const Tableau<U> &autre) {
Tableau{ autre }.swap(*this);
return *this;
}
~Tableau() {
delete[] elems;
}
void swap(Tableau &autre) noexcept {
using std::swap;
swap(elems, autre.elems);
swap(nelems, autre.nelems);
swap(cap, autre.cap);
}
Tableau& operator=(const Tableau &autre) {
Tableau{ autre }.swap(*this);
return *this;
}
reference operator[](size_type n) noexcept {
return elems[n];
}
const_reference operator[](size_type n) const noexcept {
return elems[n];
}
void push_back(const_reference val) {
if (full()) grow();
elems[size()] = val;
++nelems;
}
private:
void grow() {
const size_type new_cap = capacity() ? capacity() * 2 : 128; // hum
auto p = new value_type[new_cap];
try {
std::copy(begin(), end(), p);
delete[]elems;
cap = new_cap;
elems = p;
} catch(...) {
delete [] p;
throw;
}
}
public:
bool operator==(const Tableau &autre) const {
return size() == autre.size() &&
std::equal(begin(), end(), autre.begin());
}
bool operator!=(const Tableau &autre) const {
return !(*this == autre);
}
Tableau(Tableau &&autre) noexcept
: elems{ std::exchange(autre.elems, nullptr) },
nelems{ std::exchange(autre.nelems, 0) },
cap{ std::exchange(autre.cap, 0) } {
}
Tableau& operator=(Tableau &&autre) noexcept {
Tableau{ std::move(autre) }.swap(*this);
return *this;
}
};
Pour le code (plus simple, tout aussi efficace) que l'on obtient si on ajoute un
unique_ptr
à notre tableau générique,
voici :
#include <cstddef>
#include <algorithm>
#include <initializer_list>
#include <memory>
template <class T>
class Tableau {
public:
using value_type = T;
using size_type = std::size_t;
using pointer = value_type*;
using const_pointer = const value_type*;
using reference = value_type&;
using const_reference = const value_type&;
private:
std::unique_ptr<value_type[]> elems;
size_type nelems{},
cap{};
public:
[[nodiscard]] size_type size() const noexcept {
return nelems;
}
[[nodiscard]] size_type capacity() const noexcept {
return cap;
}
[[nodiscard]] bool empty() const noexcept {
return !size();
}
private:
[[nodiscard]] bool full() const noexcept {
return size() == capacity();
}
public:
using iterator = pointer;
using const_iterator = const_pointer;
[[nodiscard]] iterator begin() noexcept {
return elems.get();
}
[[nodiscard]] const_iterator begin() const noexcept {
return elems.get();
}
[[nodiscard]] const_iterator cbegin() const noexcept {
return elems.get();
}
[[nodiscard]] iterator end() noexcept {
return begin() + size();
}
[[nodiscard]] const_iterator end() const noexcept {
return begin() + size();
}
[[nodiscard]] const_iterator cend() const noexcept {
return begin() + size();
}
Tableau() = default;
Tableau(std::initializer_list<value_type> lst)
: elems{ new value_type[lst.size()] },
nelems{ lst.size() }, cap{ lst.size() } {
std::copy(lst.begin(), lst.end(), begin());
}
Tableau(size_type n, const value_type &init)
: cap{ n }, nelems{ n }, elems{ new value_type[n] } {
std::fill(begin(), end(), init);
}
Tableau(const Tableau &autre)
: elems{ new value_type[autre.size()] },
nelems{ autre.size() }, cap{ autre.size() } {
std::copy(autre.begin(), autre.end(), begin());
}
template <class It>
Tableau(It debut, It fin)
: nelems(std::distance(debut, fin)) {
cap = size();
elems = make_unique<value_type[]>{ size() };
std::copy(debut, fin, begin());
}
template <class U>
Tableau(const Tableau<U> &autre)
: cap{ autre.size() }, nelems{ autre.size() },
elems{ new value_type[autre.size()] } {
std::copy(autre.begin(), autre.end(), begin());
}
template <class U>
Tableau& operator=(const Tableau<U> &autre) {
Tableau{ autre }.swap(*this);
return *this;
}
~Tableau() = default;
void swap(Tableau &autre) noexcept {
using std::swap;
swap(elems, autre.elems);
swap(nelems, autre.nelems);
swap(cap, autre.cap);
}
Tableau& operator=(const Tableau &autre) {
Tableau{ autre }.swap(*this);
return *this;
}
reference operator[](size_type n) noexcept {
return elems[n];
}
const_reference operator[](size_type n) const noexcept {
return elems[n];
}
void push_back(const_reference val) {
if (full()) grow();
elems[size()] = val;
++nelems;
}
private:
void grow() {
using namespace std;
const size_type new_cap = capacity() ? capacity() * 2 : 128;
unique_ptr<value_type[]> p{ new value_type[new_cap] };
copy(begin(), end(), p);
cap = new_cap;
swap(p, elems);
}
public:
bool operator==(const Tableau &autre) const {
return size() == autre.size() &&
std::equal(begin(), end(), autre.begin());
}
bool operator!=(const Tableau &autre) const {
return !(*this == autre);
}
Tableau(Tableau &&autre) = default;
Tableau& operator=(Tableau &&autre) = default;
};
Si vous souhaitez faire des exercices, dans les notes de cours :
- Le coeur de notre réflexion se trouvera dans le document
POO – Volume 02, pp. 96-115
Note : dû à la grève de la STM, nous
tiendrons cette séance à distance par Teams. |
S05
|
Mercredi 1 oct. 8 h 30-11 h 30
|
Au menu, on met la classe Tableau<T> de
côté aujourd'hui pour se donner quelques outils :
Si vous souhaitez faire des exercices, dans les notes de cours :
Note : dû à la grève de la STM, nous
tiendrons cette séance à distance par Teams.
|
S06
|
Mercredi 8 oct. 13 h-16 h
|
Au menu :
|
S07
|
Mercredi 15 oct. 8 h 30-11 h 30
|
|
S08
|
Mercredi 22 oct. 8 h 30-11 h 30
|
|
S09
|
Mercredi 29 oct. 13 h-16 h
|
|
S10
|
Mercredi 5 nov. 8 h 30-11 h 30
|
|
S11
|
Mercredi 12 nov. 8 h 30-11 h 30
|
|
S12
|
Mercredi 19 nov. 13 h-16 h
|
|
S13
|
Mercredi 26 nov. 8 h 30-11 h 30
|
|
S14
|
Lundi 1 déc. 13 h-16 h
|
Chic examen plein d'amour
|
« S15 »
|
|
Semaine de finalisation et de présentation du projet, avec présentation
(date et heure à venir)
|
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.
Les consignes des travaux pratiques en lien avec le cours sont ci-dessous.
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 :
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 :
- Des calculs redondants (la distance entre deux points est potentiellement
évaluée deux fois par point candidat; la gestion du candidat
0 est discutable)
- Avertissements à la compilation (comparaison signé / non-signé)
- Code très manuel, auquel il faut réfléchir pour saisir l'intention
- En plus, les paramètres sont passés par copie plutôt que par
référence-vers-const, ce qui coûte cher pour rien
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!
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 :