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
|
Jeudi 31 août 8 h 30-11 h 30
|
Au menu :
- Brève présentation du plan
de cours
- Présentation sommaire des travaux de la session (TP00
et
TP01)
- Discussion de ce que signifie
être OO,
selon vos divers points de vue et vos expériences préalables. C'est un
sujet complexe en soi et simplement en discuter est un terreau fertile
pour faire réagir les neurones
- Jongler avec un petit programme de devinette,
pour dérouiller nos articulations et notre cerveau....
-
Utiliser {} ou utiliser
()?
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 7 sept. 9 h-12 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
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
|
Jeudi 14 sept. 9 h-12 h
|
Au menu :
Le code de ce matin, 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_ {} sorte_sequentielle;
class sorte_recycleuse_ {} sorte_recycleuse;
class 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);
return id;
}
void rendre(value_type id) override {
if (find(begin(disponibles), end(disponibles), id) != end(disponibles))
throw non_attribue{};
disponibles.push_back(id);
}
public:
GenerateurAleatoire() : disponibles(static_cast<int>(id_max) - id_min + 1) {
iota(begin(disponibles), end(disponibles), id_min);
}
};
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
|
Jeudi 21 sept. 9 h-12 h
|
À 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.
|
S04
|
Jeudi 28 sept. 9 h-12 h
|
Au menu :
- Q02
- 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
|
|
Jeudi 5 oct. 9 h-12 h
|
Pas de cours avec moi cette semaine car je suis à
CppCon et j'ai les mains pleines. Vous
pouvez suivre mes aventures au quotidien sur
../../Sujets/Orthogonal/cppcon2023.html
|
S05
|
Jeudi 12 oct. 9 h-12 h
|
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 :
|
S06
|
Jeudi 19 oct. 9 h-12 h
|
Au menu :
|
S07
|
Jeudi 26 oct. 8 h 30-11 h 30
|
Au menu :
Pour vous divertir, un petit exercice :
- Écrivez le trait nbits_traits<T>
tel que nbits_traits<T>::value sera le nombre de bits utilisés pour représenter un T
- Pour implémenter ce trait, considérez que :
-
sizeof(T) est le nombre de bytes qu'occupe un T
en mémoire
- le nombre de bits dans un byte n'est pas nécessairement huit (même si ce l'est dans l'immense majorité des cas). Techniquement, le nombre de bits dans un
byte est décrit par par la constante std::numeric_limits<unsigned char>::digits
- puisqu'on ne peut instancier un void, faites en sorte que nbits_traits<void>::value
soit zéro
- Écrivez une constante générique nbits_traits_v<T>
équivalente à nbits_traits<T>::value
- Écrivez le trait same_size<T,U>
tel que same_size<T,T>::value soit vrai et que same_size<T,U>::value
soit vrai seulement si nbits_traits<T>::value==nbits_traits<U>::value
- Écrivez une constante générique same_size_v<T,U>
équivalente à same_size<T,U>::value
- Écrivez le trait suffix_trait<T>
tel que :
- en général, ce trait ne soit pas défini et corresponde à un type incomplet (donc que le nom soit suivi d'un simple « ; », même pas d'accolades)
- la méthode de classe suffix_trait<T>::value()
retourne une std::string ayant pour valeur un suffixe valide pour T
lorsque les littéraux de ce type portent effectivement un suffixe ("l"
pour long, "ll"
pour long long, "u"
pour unsigned int, "ul"
pour unsigned long, "ull"
pour unsigned long long, "f"
pour float et "l"
pour long double)
// ...
#include <iostream>
#include <string>
using namespace std;
template <class T>
void test(ostream &os, const string &nom) {
os << "Une instance du type " << nom << " occupe " << nbits_traits_v<T> << " bits en memoire\n";
if constexpr (same_size_v<T,int>) {
os << "\t... c'est autant qu'un int!" << endl;
} else {
os << "\t... ce n'est pas autant qu'un int" << endl;
}
}
int main() {
test<short>(cout, "short");
test<float>(cout, "float");
test<double>(cout, "double");
test<string>(cout, "string");
// ceci ne compilerait pas
// cout << "Le suffixe d'un littéral int est : " << suffix_trait<int>::value() << endl;
cout << "Le suffixe d'un littéral float est : " << suffix_trait<float>::value() << endl;
cout << "Le suffixe d'un littéral long double est : " << suffix_trait<long double>::value() << endl;
cout << "Le suffixe d'un littéral unsigned int est : " << suffix_trait<unsigned int>::value() << endl;
}
Note : si vous le souhaitez, vous pouvez créer les
variables constexpr qui permettent d'exprimer le
même code plus simplement, soit :
// ...
#include <iostream>
#include <string>
using namespace std;
template <class T>
void test(ostream &os, const string &nom) {
os << "Une instance du type " << nom << " occupe " << nbits_traits_v<T> << " bits en memoire\n";
if constexpr (same_size_v<T,int>) {
os << "\t... c'est autant qu'un int!" << endl;
} else {
os << "\t... ce n'est pas autant qu'un int" << endl;
}
}
int main() {
test<short>(cout, "short");
test<float>(cout, "float");
test<double>(cout, "double");
test<string>(cout, "string");
// ceci ne compilerait pas
// cout << "Le suffixe d'un littéral int est : " << suffix_trait_v<int> << endl;
cout << "Le suffixe d'un littéral float est : " << suffix_trait_v<float> << endl;
cout << "Le suffixe d'un littéral long double est : " << suffix_trait_v<long double> << endl;
cout << "Le suffixe d'un littéral unsigned int est : " << suffix_trait_v<unsigned int> << endl;
}
|
S08
|
Jeudi 2 nov. 9 h-12 h
|
Au menu :
- Q05
- Introduction au
clonage :
- Activité pratique :
- prenez comme base de travail la classe
Tableau<T> (la version avec pointeur brut, pas la version avec un
unique_ptr)(la version avec pointeur brut, pas la version avec un
unique_ptr)
- notez que nous avions mis en place une stratégie fixe de croissance
de la capacité du conteneur – la fonction grow()
doublait la capacité du Tableau<T>)
- cependant, doubler n'est pas toujours la meilleure option (c'est
efficace en termes de vitesse, mais beaucoup moins efficace en termes de
consommation de mémoire – c'est une stratégie gourmande!)
- vote tâche est de proposer une stratégie pour que le code client
puisse choisir la stratégie de croissance d'un
Tableau<T>, et de montrer le code client de votre classe une fois
votre stratégie appliquée
- nous discuterons de différentes options en fin de séance
- Étude de
diverses stratégies envisageables pour prendre en charge la
stratégie de croissance au besoin d'un tableau dynamique
- l'ordre de présentation des approches n'est pas tant de la pire à
la meilleure mais bien de la plus susceptible d'être familière à la
plus susceptible d'être surprenante (mon évaluation; vous me direz si
je me suis trompé)
- La liste d'approches pour implémenter une stratégie de
croissance couvertes en classe (à partir de la version dite
« vanille ») inclut :
- Cette liste n'est pas exhaustive, mais permet de comparer diverses approches
(c'est un cours de conception
OO,
après tout!). Et il existe des solutions plus simples
- Discussion de la surcharge des opérateurs new, delete, new[]
et delete[] de diverses manières
et sous divers angles :
Si vous souhaitez faire des exercices, dans les notes de cours :
- Le clonage
est discuté dans le document POO –
Volume 01, pp. 204-214
- La gestion avancée de la mémoire est décrite
dans le document POO – Volume
03, pp. 148-195
|
S09
|
Jeudi 9 nov. 9 h-12 h
|
Au menu :
- Q06
- Retour sur Q05
- Retour sur le détecteur de fuite et sur l'enjeu de la venue au
monde d'un objet
- Discussion de la surcharge des opérateurs new, delete, new[]
et delete[] de diverses manières
et sous divers angles :
- Poursuite de notre étude des mécanismes de gestion avancée de la
mémoire sous divers angles
- Retour sur un tableau dynamique générique, en tenant compte
des mécanismes que nous avons couverts à la séance
S09
- quelques fonctionnalités de gestion de mémoire brute
- Examen de ce que font les conteneurs standards, par exemple
std::vector
et
std::list, à
l'aide d'allocateurs standards
- Difficultés intrinsèques à la saine gestion de la mémoire dans un
tel conteneur
- cas types des méthodes insert() et
erase()
- Recours au mouvement ou à la copie pour les éléments
- Direction des copies et des
mouvements
Si vous souhaitez faire des exercices, dans les notes de cours :
- La gestion avancée de la mémoire est décrite
dans le document POO – Volume
03, pp. 148-195
Une infrastructure
d'expérimentation est disponible ici si vous souhaitez jouer
avec diverses techniques de gestion de la mémoire allouée
dynamiquement. Amusez-vous bien!
Je donne parfois à titre de minitest pratique ce que vous trouverez
sur Exercice-Orque-Memoire.html
mais je ne le ferai pas cette session faute de temps. Vous pouvez quand
même vous amuser à faire l'exercice pour voir ce à quoi vous arriverez,
et pour en discuter entre vous / avec moi si vous en avez envie!
|
S10
|
Jeudi 16 nov. 8 h 30-11 h 30
|
Au menu :
|
S11
|
Jeudi 23 nov. 13 h-16 h
|
Au menu :
- Q08
- On sait comment écrire min(T,T), mais
comment pourrait-on écrire
min(T,U)?
- Excursion dans le monde de la métaprogrammation avec
std::declval<T>() et
std::common_type<T,U>
- Réaliser une division entière en C++ (oui, il y a des trucs amusants
à faire avec ça!)
- Implémenter des multiméthodesmultiméthodes dans un langage
OO
Pour le code exploré avec
std::variant, qui a semblé vous plaire, nous sommes
passés de cette version (polymorphique classique) :
#include <iostream>
using namespace std;
struct Sphere;
struct Box;
struct AABB;
struct Volume {
virtual bool collides(const Volume&) const = 0;
virtual bool collides(const Sphere&) const = 0;
virtual bool collides(const Box&) const = 0;
virtual bool collides(const AABB&) const = 0;
virtual ~Volume() = default;
};
struct Sphere : Volume {
bool collides(const Volume &autre) const override {
return autre.collides(*this);
}
bool collides(const Sphere &autre) const override {
cout << "Sphere x Sphere" << endl;
return true;
}
bool collides(const Box &autre) const override {
cout << "Sphere x Box" << endl;
return true;
}
bool collides(const AABB &autre) const override {
cout << "Sphere x AABB" << endl;
return true;
}
};
struct Box : Volume {
bool collides(const Volume &autre) const override {
return autre.collides(*this);
}
bool collides(const Sphere &autre) const override {
cout << "Box x Sphere" << endl;
return true;
}
bool collides(const Box &autre) const override {
cout << "Box x Box" << endl;
return true;
}
bool collides(const AABB &autre) const override {
cout << "Box x AABB" << endl;
return true;
}
};
struct AABB : Volume {
bool collides(const Volume &autre) const override {
return autre.collides(*this);
}
bool collides(const Sphere &autre) const override {
cout << "AABB x Sphere" << endl;
return true;
}
bool collides(const Box &autre) const override {
cout << "AABB x Box" << endl;
return true;
}
bool collides(const AABB &autre) const override {
cout << "AABB x AABB" << endl;
return true;
}
};
class Obj3D {
// ...
public:
virtual const Volume &volume() const = 0;
virtual ~Obj3D() = default;
};
class Mario : public Obj3D {
Box box;
const Volume &volume() const override { return box; }
};
class Pacman : public Obj3D {
Sphere sphere;
const Volume &volume() const override { return sphere; }
};
void test(const Obj3D &a, const Obj3D &b) {
if (a.volume().collides(b.volume()))
;
}
int main() {
test(Mario{}, Pacman{});
}
... à cette version avec
std::variant :
#include <iostream>
#include <variant>
using namespace std;
struct Sphere {};
struct Box {};
struct AABB {};
using Volume = variant<Sphere, Box, AABB>;
class Obj3D {
// ...
public:
virtual Volume volume() const = 0;
virtual ~Obj3D() = default;
};
class Mario : public Obj3D {
Box box;
Volume volume() const override { return box; }
};
class Pacman : public Obj3D {
Sphere sphere;
Volume volume() const override { return sphere; }
};
struct Visiteur {
bool operator()(Sphere, Sphere) const { cout << "Sphere x Sphere" << endl; return true; }
bool operator()(Sphere, Box) const { cout << "Sphere x Box" << endl; return true; }
bool operator()(Sphere, AABB) const { cout << "Sphere x AABB" << endl; return true; }
bool operator()(Box, Sphere) const { cout << "Box x Sphere" << endl; return true; }
bool operator()(Box, Box) const { cout << "Box x Box" << endl; return true; }
bool operator()(Box, AABB) const { cout << "Box x AABB" << endl; return true; }
bool operator()(AABB, Sphere) const { cout << "AABB x Sphere" << endl; return true; }
bool operator()(AABB, Box) const { cout << "AABB x Box" << endl; return true; }
bool operator()(AABB, AABB) const { cout << "AABB x AABB" << endl; return true; }
};
bool collides(Volume v0, Volume v1) {
return visit(Visiteur{}, v0, v1);
}
void test(const Obj3D &a, const Obj3D &b) {
if (collides(a.volume(), b.volume()))
;
}
int main() {
test(Mario{}, Pacman{});
}
... à cette version encore plus simple (dans cet exemple hyper
simpliste) aussi avec
std::variant :
#include <iostream>
#include <variant>
using namespace std;
struct Sphere {};
struct Box {};
struct AABB {};
ostream &operator<<(ostream &os, Sphere) { return os << "Sphere"; }
ostream &operator<<(ostream &os, Box) { return os << "Box"; }
ostream &operator<<(ostream &os, AABB) { return os << "AABB"; }
using Volume = variant<Sphere, Box, AABB>;
class Obj3D {
// ...
public:
virtual Volume volume() const = 0;
virtual ~Obj3D() = default;
};
class Mario : public Obj3D {
Box box;
Volume volume() const override { return box; }
};
class Pacman : public Obj3D {
Sphere sphere;
Volume volume() const override { return sphere; }
};
struct Visiteur {
template <class T, class U>
bool operator()(T a, U b) const { cout << a << " x " << b << endl; return true; }
};
bool collides(Volume v0, Volume v1) {
return visit(Visiteur{}, v0, v1);
}
void test(const Obj3D &a, const Obj3D &b) {
if (collides(a.volume(), b.volume()))
;
}
int main() {
test(Mario{}, Pacman{});
}
... ou encore :
#include <iostream>
#include <variant>
using namespace std;
struct Sphere {};
struct Box {};
struct AABB {};
ostream &operator<<(ostream &os, Sphere) { return os << "Sphere"; }
ostream &operator<<(ostream &os, Box) { return os << "Box"; }
ostream &operator<<(ostream &os, AABB) { return os << "AABB"; }
using Volume = variant<Sphere, Box, AABB>;
class Obj3D {
// ...
public:
virtual Volume volume() const = 0;
virtual ~Obj3D() = default;
};
class Mario : public Obj3D {
Box box;
Volume volume() const override { return box; }
};
class Pacman : public Obj3D {
Sphere sphere;
Volume volume() const override { return sphere; }
};
bool collides(Volume v0, Volume v1) {
return visit([](auto a, auto b) { cout << a << " x " << b << endl; }, v0, v1);
}
void test(const Obj3D &a, const Obj3D &b) {
if (collides(a.volume(), b.volume()))
;
}
int main() {
test(Mario{}, Pacman{});
}
Pour le côté plus divertissant, nous avons aussi fait :
#include <iostream>
#include <variant>
using namespace std;
struct Viz {
template <class T>
void operator()(T val) const { cout << val << endl; }
void operator()(const string &s) { cout << '\"' << s << '\"' << endl; }
};
int main() {
variant<int, float, string> v = 3;
visit(Viz{}, v);
v = "Coucou";
visit(Viz{}, v);
visit([](auto &&val) { cout << val << endl; }, v);
}
... de même que :
#include <iostream>
#include <variant>
using namespace std;
template <class ... Ts>
struct combine : Ts... {
combine(Ts... ps) : Ts{ ps }...;
using Ts::operator()...;
};
int main() {
variant<int, float, string> v = 3;
visit(combine(
[](auto val) { cout << val << endl; },
[](const string &s) { cout << '\"' << s << '\"' < endl; }
), v);
}
#include <algorithm>
#include <variant>
#include <iostream>
using namespace std;
struct Plus {} plus_;
struct Fois {} fois_;
using valeur = variant<int, string>;
using operateur = variant<Plus, Fois>;
template <class ... Ts>
struct combine : Ts... {
combine(Ts... ps) : Ts{ ps }...;
using Ts::operator()...;
};
int main() {
auto op = combine(
[](int a, Plus, int b) -> valeur { return a + b; },
[](int a, Fois, int b) -> valeur { return a * b; },
[](const string &a, Plus, const string &b) -> valeur { return a + b; },
[](auto, auto, auto) -> valeur { throw 3; }
);
valeur a = 3, b = 3;
operateur oper = plus_;
auto res = visit(op, a, oper, b);
visit([](auto &&x) { cout << x << endl; }, res);
oper = fois_;
res = visit(op, a, oper, b);
visit([](auto &&x) { cout << x << endl; }, res);
a = "J'aime"s, b = " mon prof!"s;
oper = plus_;
res = visit(op, a, oper, b);
visit([](auto &&x) { cout << x << endl; }, res);
try {
oper = fois_;
res = visit(op, a, oper, b);
visit([](auto &&x) { cout << x << endl; }, res);
} catch(...) {
cerr << "Oups!\n";
}
}
Enfin, un petit comparatif de vitesse :
C'est quand même divertissant, non?
|
S12
|
Jeudi 30 nov. 9 h-12 h
|
Au menu :
- Q09 (ne le faites pas avant la fin du cours, car la séance d'aujourd'hui contient des indices!)
- Retour sur Q07
- Comment créer une DLL et en
exposer des services (laboratoire dirigé)
- Que signifie extern "C"?
- Comment éviter le code redondant en implémentant des opérateurs
relationnels?
Pour s'amuser...
- Quelques énigmes à résoudre. Vous y réfléchissez, vous proposez votre
approche, je vous propose un truc, on discute...
Énigme A : un(e)
collègue vous propose le code suivant :
#ifndef FORME_H
#define FORME_H
//
// Forme.h
//
#include <iosfwd>
class Forme {
public:
using value_type = char;
private:
value_type symbole_{ '*' };
public:
Forme() = default;
Forme(value_type symbole) : symbole_{symbole} {
}
value_type symbole() const {
return symbole_;
}
virtual void dessiner(std::ostream &os) const = 0;
virtual ~Forme() = default;
};
void afficher_formes(Forme *tab, std::size_t n);
#endif
//
// Forme.cpp
//
#include "Forme.h"
#include <iostream>
using namespace std;
ostream& operator<<(ostream &os, const Forme &f) {
return f.dessiner(os), os;
}
void afficher_formes(Forme *tab, size_t n) {
for (auto p = tab; p != tab + n; ++p)
cout << *p << endl;
}
//
// Rectangle.h
//
#ifndef RECTANGLE_H
#define RECTANGLE_H
#include "Forme.h"
template <class T>
constexpr bool est_entre_inclusif(T candidat, T minVal, T maxVal) {
return minVal <= candidat && candidat <= maxVal;
}
class DimensionIncorrecte {};
#include <ostream>
class Rectangle : public Forme {
public:
using size_type = int;
private:
static constexpr const size_type MIN_LARGEUR = 1, MAX_LARGEUR = 80;
static constexpr const size_type MIN_HAUTEUR = 1, MAX_HAUTEUR = 25;
size_type largeur_,
hauteur_;
static constexpr size_type valider_largeur(size_type largeur) {
return est_entre_inclusif(largeur, MIN_LARGEUR, MAX_LARGEUR)? largeur : throw DimensionIncorrecte{};
}
static constexpr size_type valider_hauteur(size_type hauteur) {
return est_entre_inclusif(hauteur, MIN_HAUTEUR, MAX_HAUTEUR)? hauteur : throw DimensionIncorrecte{};
}
public:
Rectangle(size_type largeur, size_type hauteur)
: largeur_{ valider_largeur(largeur) },
hauteur_{ valider_hauteur(hauteur) }
{
}
Rectangle(size_type largeur, size_type hauteur, value_type symbole)
: Forme{ symbole },
largeur_{ valider_largeur(largeur) },
hauteur_{ valider_hauteur(hauteur) }
{
}
size_type largeur() const {
return largeur_;
}
size_type hauteur() const {
return hauteur_;
}
void dessiner(std::ostream &os) const {
using std::endl;
for (size_type hau = 0; hau < hauteur(); ++hau) {
for (size_type lar = 0; lar < largeur(); ++lar)
os << symbole();
os << endl;
}
}
};
#endif
//
// Principal.cpp
//
#include "Rectangle.h"
#include "Forme.h"
int main() {
Rectangle tab[] {
Rectangle{ 1, 3 }, Rectangle{ 4, 2 }, Rectangle{ 3, 3 }
};
enum { N = sizeof(tab) / sizeof(tab[0]) };
afficher_formes(tab, N); // BOUM!
}
Ce collègue vous informe que ce code compile sans avertissement mais, à
sa grande surprise, plante sauvagement à l'exécution, apparemment une fois
le 1er Rectangle affiché à la console. Quelques questions se posent :
- Pourquoi ce code est-il syntaxiquement correct mais sémantiquement
incorrect?
- Comment pourrait-on le corriger?
- À quoi devrait-on faire attention dans le futur pour ne pas revivre de
tels ennuis?
Énigme B :
il arrive souvent qu'on ait besoin d'une zone tampon temporaire (un Buffer
temporaire, en jargon) pour entreposer des données. Supposons que
nous ayons une politique à l'effet que, si nous avons besoin de
4 Ko (4096 bytes) ou
moins, nous souhaitions allouer ce tampon sur la pile (classe
std::array ou tableau brut, à votre convenance) alors que
si nous avons besoin de plus d'espace, nous souhaitions privilégier
un vecteur. Nous voulons donc que, dans le code ci-dessous, présumant
sizeof(int)==4 et
sizeof(double)==8,
lbi soit un tableau de 1000 int et
que lbd soit un vecteur de
1000 double. Comment y arriver?
#include "local_buffer.h" // <-- votre contribution
#include <algorithm>
int main() {
using namespace std;
auto lbi = create_local_buffer<int,1000>(); // lbi --> array<int,1000>
auto lbd = create_local_buffer<double, 1000>(); // lbd --> vector<double>
fill(begin(lbi), end(lbi), -1);
fill(begin(lbd), end(lbd), 3.14159);
}
Énigme C : vous souhaitez
détecter dynamiquement les erreurs de conversion menant à des pertes
d'information, comme par exemple dans le programme suivant :
#include <iostream>
#include <limits>
int main() {
using std::numeric_limits;
short s = numeric_limits<short>::max();
int i = s;
++i;
s = i; // <-- ICI
// ...
}
Proposez une approche permettant, à l'aide d'une syntaxe semblable à celle d'un
opérateur de transtypage ISO, de faire en sorte qu'on
puisse ne permettre une telle conversion que si elle ne mène pas à une
perte d'information ou à une erreur de calcul. Par exemple, en
supposant que cette opération se nomme checked_cast, on aurait :
#include <iostream>
#include <limits>
int main() {
using std::numeric_limits;
short s = numeric_limits<short>::max();
int i = s;
++i;
s = checked_cast<short>(i); // <-- ICI (lèverait une exception)
// ...
}
Petit complément : prenez soin de réfléchir aux cas pour lesquels votre approche serait applicable et aux cas pour
lesquels elle ne le serait pas, du moins pas raisonnablement. Êtes-vous en mesure de valider certains de
ces a priori dès la compilation du code client?
Énigme D : soit la
classe Tableau<T,N> suivante :
#include <algorithm>
template <class T, std::size_t N>
class Tableau {
public:
using value_type = T;
using size_type = std::size_t;
using iterator = value_type*;
using const_iterator = const value_type*;
private:
value_type elems[N]{};
public:
constexpr size_type size() const {
return N;
}
iterator begin() noexcept {
return std::begin(elems);
}
const_iterator begin() const noexcept {
return std::begin(elems);
}
iterator end() noexcept {
return std::end(elems);
}
const_iterator end() const noexcept {
return std::end(elems);
}
Tableau() = default;
template <class U>
Tableau(const Tableau<U,N> &autre) {
using std::copy;
copy(autre.begin(), autre.end(), begin());
}
//
// Sainte-Trinité implicitement Ok
//
value_type& operator[](size_type n) {
return elems[n];
}
value_type operator[](size_type n) const {
return elems[n];
}
bool operator==(const Tableau &autre) const {
using std::equals;
return equals(begin(), end(), autre.begin());
}
bool operator!=(const Tableau &autre) const {
return !(*this == autre);
}
};
On souhaite savoir quelle est la meilleure manière d'implémenter les opérateurs ==
et != dans les cas suivants :
- Si on compare un Tableau<T,N> avec un Tableau<T,M>
pour M != N
- Si on compare un Tableau<T,N> avec un Tableau<U,N>
pour deux types T et
U distincts, et
- Si on compare un Tableau<T,N> avec un Tableau<U,M>
pour M != N et deux types
T et U distincts
Énigme E : vous avez conçu une classe Tableau<T>
dans laquelle vous
avez implémenté l'opérateur <. En version abrégée, cela vous donne :
#include <cstddef>
template <class T>
class Tableau {
public:
using value_type = T;
using size_type = std::size_t;
using iterator = value_type*;
using const_iterator = const value_type*;
private:
value_type *elems{};
size_type nelems{}, cap{};
public:
// ...
bool operator<(const Tableau &autre) const {
for(auto p = begin(), q = autre.begin(); p != end() && q != autre.end(); ++p, ++q)
if (*p != *q)
return *p < *q;
return p == end() && q != autre.end();
}
};
Cela dit, vous constatez rapidement qu'implémenter les opérateurs <=, >
et >= en copiant / collant le code de l'opérateur <
puis en ajustant ce
code est... périlleux. Comment pourriez-vous simplifier votre existence?
Énigme F : vous écrivez
une fonction variadique acceptant des paramètres de divers types, et vous
souhaitez que cette fonction utilise un tampon dont la taille (exprimée en
bytes) soit suffisante pour contenir le plus gros de ces paramètres, et
dont l'alignement corresponde à l'alignement le plus strict de celui de
tous ces types. Comment écrirez-vous cette fonction? (ne vous occupez que
de la signature de la fonction et de la définition du tampon)
En espérant que tout cela vous ait diverti!
|
S13
|
Jeudi 30 nov. 13 h-16 h
|
Au menu :
- Retour sur Combine<Ts...>
- Systématisation de la
composition de fonctions
- Évolutions des pratiques au fil de l'évolution du langage
- Quelques bribes de
métaprogrammation pour nous permettre de faire des choix éclairés...
- Par exemple (et ce n'est qu'un exemple), comment une implémentation
de std::copy() peut-elle choisir entre
utiliser std::memcpy() et faire une boucle
réalisant des affectations élément par élément?
- Conséquemment, pourquoi devrions-nous (presque) systématiquement utiliser std::copy()
(et les fonctions du même acabit) plutôt que de
chercher à l'optimiser?
- De manière corollaire : comment permettre à std::copy(), à ses cousines et à nos conteneurs d'être aussi
efficaces que possible avec nos objets?
- Le C++ Detection Idiom
Pour finir la session, quelques activités pratiques...
Activité 00
Écrivez une classe Valideur<T> capable de prendre à la construction un nombre arbitrairement grand de critères et qui se comporte comme un prédicat. Par critère, on entend ici une opération applicable à un T
et retournant true seulement si l'instance de T
reçue en paramètre respecte le critère en question
Un exemple de code de test serait :
#include <string>
#include <locale>
#include <algorithm>
#include <iterator>
//
// AJOUTEZ LES INCLUSIONS SOUHAITÉES
//
using namespace std;
//
// VOTRE CODE VA ICI
//
struct est_non_nul {
template <class T>
constexpr bool operator()(const T *p) const {
return !!p;
}
};
bool est_de_longueur_raisonnable(const string *p) {
return p && p->size() < 20; // arbitraire, pour faire un exemple
}
auto sans_majuscules = [](const string *s) -> bool {
const auto &loc = locale{ "" };
return !s ||
find_if(begin(*s), end(*s), [&](char c) {
return isupper(c, loc);
}) == end(*s);
};
#include <cassert>
int main() {
Valideur<const string*> v0 { est_non_nul{} };
// notez les parenthèses (c'est volontaire)
Valideur<const string*> v1 (est_non_nul{}, est_de_longueur_raisonnable,sans_majuscules);
string s0 = "Allo",
s1 = "allo",
s2 = "allo les amis ceci est une chaine de longueur deraisonnable";
string *p = {};
assert(v0(&s0) && !v1(&s0));
assert(v0(&s1) && v1(&s1));
assert(v0(&s2) && !v1(&s2));
assert(!v0(p) && !v1(p));
}
Quelques solutions possibles :
Activité 01
Écrivez une classe case_insensitive_string qui se comporte comme une
std::string à ceci près que les comparaisons (opérateurs relationnels) ne
tiennent pas compte de la casse, donc :
// ...
int main() {
case_insensitive_string s0 {"allo"}, s1{"Allo"};
if(s0 == s1)
cout << s0 << " == " << s1 << endl; // passera ici
else
cout << s0 << " != " << s1 << endl;
}
Activité 02
Écrivez une fonction concatener() acceptant en paramètre un nombre variadique de chaînes de caractères. Faites en sorte d'offrir une
version acceptant des std::basic_string<C>. Faites en sorte d’offrir une version acceptant des std::basic_string_view<C>.
Activité 03
Faites en sorte que le code suivant :
#include <iterator>
#include <string>
#include <iostream>
using namespace std;
//
// VOTRE CODE VA ICI
//
int main() {
for(auto && c : inverseur{ "J'aime mon prof"s })
cout << c;
}
... compile et affiche :
Activité 04
Écrivez l'algorithme filtrer_si(deb,fin,pred)
tel que ceci :
// ...
int main() {
int tab[]{ 2,3,5,7,11 };
for(auto n : filtrer_si(begin(tab), end(tab), [](int n) { return n % 2 == 0; }))
cout << n << ' ';
}
... affichera cela :
Solution possible :
https://wandbox.org/permlink/nAsKppV3Sx4q9pcI
Notez que cette solution a un irritant non-négligeable : nous avons
choisi le type du substrat retourné pour le code client, ce qui peut ne
pas être approprié pour certains cas d'utilisation. Comment pourrions-nous
laisser le code client faire ce choix?
Solutions possibles :
Activité 05
Écrivez un type représentant une liste simplement chaînée de
T, incluant les itérateurs pour la parcourir. Expliquez vos choix
d'implémentation.
|
S14
|
Jeudi 7 déc. 9 h-12 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.
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 :