Copy of https://perso.isima.fr/loic/cpp/tp04.en.php
tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
Institut d'informatique ISIMA
  • twitter
  • linkedin
  • viadeo

[C++] TP 4

 This page might be outdated. Please email me if you found something incorrect.
Lire en Français

First publication date : 2020/04/17

The skeleton of the two exercices can be cloned from here (not translated) :

git clone https://gitlab.com/kiux/CPP4.git

Red string ...

Vous avez écrit les classes Rectangle et Cercle. L'idée est de mettre désormais en place l'héritage pour qu'un rectangle (respectivement un cercle) soit une forme. Nous allons ajouter également la composition/agrégation avec la classe Point

Classe Point

Une instance particulière de la classe Point, ORIGINE sera définie et utilisée. Cette origine servira de référence quand la position de la forme ne sera pas précisée à la construction (une constante globale fait l'affaire).

TEST_CASE("Instanciation", "[Point]") {
  Point p1;
  REQUIRE(p1.getX() == 0);
  REQUIRE(p1.getY() == 0);
  
  p1.setX(11);
  p1.setY(21);

  REQUIRE(p1.getX() == 11);
  REQUIRE(p1.getY() == 21);

  Point p2(12, 22);

  REQUIRE(p2.getX() == 12);
  REQUIRE(p2.getY() ==  0);  // :-)
}

TEST_CASE("Origine", "[Point]") {
  REQUIRE(ORIGINE.getX() == 0);
  REQUIRE(ORIGINE.getY() == 0);
}
TEST(Point, Instanciation) {
  Point p1;
  ASSERT_EQ(p1.getX(), 0);
  ASSERT_EQ(p1.getY(), 0);
  
  p1.setX(11);
  p1.setY(21);

  ASSERT_EQ(p1.getX(), 11);
  ASSERT_EQ(p1.getY(), 21);

  Point p2(12, 22);

  ASSERT_EQ(p2.getX(), 12);
  ASSERT_EQ(p2.getY(),  0);  // :-)
}

TEST(Point, Origine) {
  ASSERT_EQ(ORIGINE.getX(), 0);
  ASSERT_EQ(ORIGINE.getY(), 0);
}

Si vous voulez définir ORIGINE comme un membre public constant de classe Point, c'est tout-à-fait possible, modifiez les tests en conséquence. De l'intérieur de la classe, ORIGINE est directement accessible. De l'extérieur, il faudra préfixer par le nom de la classe qui définit ce point particulier Point::ORIGINE

Classe Forme

Passons maintenant à la classe Forme. Ajoutez un attribut couleur de type COULEURS. COULEURS peut être une énumération classique (à la C) mais aussi une énumération typée (enum class), concept apparu avec la norme 2011 du langage.

enum class COULEURS {
  NOIR, BLANC
};

COULEURS couleur = COULEURS::BLANC;

Les enum class ont de nombreux avantages par rapport aux énumérations classiques dont notamment la portée limitée des valeurs (on est obligé de préfixer la valeur par le nom de l'énumération) et la non conversion implicite en entier

TEST_CASE("Instanciation1", "[Forme]") {
  Forme f1;
  REQUIRE(f1.getPoint().getX() == 0);
  REQUIRE(f1.getPoint().getY() == 0);
  REQUIRE(f1.getCouleur() ==  COULEURS::BLEU);
}

TEST_CASE("Instanciation2", "[Forme]") {
  Forme f2;
  
  f2.setX(15);
  f2.setY(25);
  f2.setCouleur(COULEURS::VERT);
  REQUIRE (f2.getPoint().getX() == 15);
  REQUIRE (f2.getPoint().getY() == 25);
  REQUIRE (f2.getCouleur() == COULEURS::VERT);
  REQUIRE_FALSE (f2.getCouleur() == COULEURS::BLEU);
  REQUIRE_FALSE (f2.getCouleur() == COULEURS::ROUGE);
  REQUIRE_FALSE (f2.getCouleur() == COULEURS::JAUNE);
}

TEST_CASE("Instanciation3", "[Forme]") {
  // SI LE TEST NE MARCHE PAS, VOUS AVEZ UNE ERREUR DANS VOTRE CODE
  Forme f2(Point(10,20), COULEURS::ROUGE);
  REQUIRE (f2.getPoint().getX() == 10);
  REQUIRE (f2.getPoint().getY() == 20);
  REQUIRE (f2.getCouleur() == COULEURS::ROUGE);
  REQUIRE_FALSE (f2.getCouleur() == COULEURS::BLEU);

  f2.getPoint().setX(15);
  f2.getPoint().setY(25);
  f2.setCouleur(COULEURS::JAUNE);
  REQUIRE (f2.getPoint().getX() == 15);
  REQUIRE (f2.getPoint().getY() == 25);
  REQUIRE (f2.getCouleur() == COULEURS::JAUNE);
  REQUIRE_FALSE (f2.getCouleur() == COULEURS::BLEU);
  REQUIRE_FALSE (f2.getCouleur() == COULEURS::ROUGE);
}
TEST(Forme, Instanciation1) {
  Forme f1;
  ASSERT_EQ(f1.getPoint().getX(), 0);
  ASSERT_EQ(f1.getPoint().getY(), 0);
  ASSERT_EQ(f1.getCouleur(), BLEU);
}

TEST(Forme, Instanciation2) { 
  Forme f2;
  
  f2.setX(15); // pas une erreur, faut essayer !!!
  f2.setY(25);
  f2.setCouleur(VERT);
  ASSERT_EQ (f2.getPoint().getX(), 15);
  ASSERT_EQ (f2.getPoint().getY(), 25);
  ASSERT_EQ (f2.getCouleur(), VERT);
  ASSERT_NE (f2.getCouleur(), BLEU);
  ASSERT_NE (f2.getCouleur(), ROUGE);
  ASSERT_NE (f2.getCouleur(), JAUNE);
}

TEST(Forme, Instanciation3) {
    // IL N'Y A PAS D'ERREUR DANS LE TEST, CELA DOIT MARCHER  
  Forme f2(Point(10,20), ROUGE);
  ASSERT_EQ (f2.getPoint().getX(), 10);
  ASSERT_EQ (f2.getPoint().getY(), 20);
  ASSERT_EQ (f2.getCouleur(), ROUGE);
  ASSERT_NE (f2.getCouleur(), BLEU);

  f2.getPoint().setX(15);
  f2.getPoint().setY(25);
  f2.setCouleur(JAUNE);
  ASSERT_EQ (f2.getPoint().getX(), 15);
  ASSERT_EQ (f2.getPoint().getY(), 25);
  ASSERT_EQ (f2.getCouleur(), JAUNE);
  ASSERT_NE (f2.getCouleur(), BLEU);
  ASSERT_NE (f2.getCouleur(), ROUGE);
}

Vous allez doter également la classe d'un attribut id qui est un indentifiant unique de la forme (on s'appuiera sur l'attribut nbFormes. Nous ne devrions pas avoir besoin de publier cet attribut de classe (dans un usage normal de la classe) mais pour les tests, il va tout de même falloir le faire. La méthode associée s'appelle prochainId() et permet un accès en lecture seulement de cet attribut.

TEST_CASE("Compteur", "[Forme]") {
   // Pour être correct, ce test doit etre le premier sur Forme
   REQUIRE(0 == Forme::prochainId());
   Forme f1;
   REQUIRE(0 == f1.getId());
   REQUIRE(1 ==  Forme::prochainId());  
   // Verification que la valeur n'est pas decrementee accidentellement.
   Forme *p = new Forme;
   REQUIRE(1 == p->getId());
   delete p;
   REQUIRE(2 == Forme::prochainId()); 
}
TEST(Forme, Compteur) {
   // Pour être correct, ce test doit etre le premier sur Forme
   ASSERT_EQ(0, Forme::prochainId());
   Forme f1;
   ASSERT_EQ(0, f1.getId());
   ASSERT_EQ(1, Forme::prochainId()); 
   // Verification que la valeur n'est pas decrementee accidentellement.
   Forme *p = new Forme;
   ASSERT_EQ(1, p->getId()); 
   delete p;
   ASSERT_EQ(2, Forme::prochainId()); 
}

Classes Rectangle et Cercle

Écrivez la relation d'héritage entre les classes Rectangle et Cercle et la classe Forme. Nettoyez les méthodes et attributs non nécessaires ! La création d'un rectangle ou d'un cercle doit incrémenter natuellement le compteur de formes.

TEST_CASE("Cercle", "[Cercle]") {
   int compteur = Forme::prochainId();
   Cercle c1;
   Cercle c2(...); 
   
   REQUIRE(c1.toString() == ".....");
   REQUIRE(c2.toString() == ".....");

   c2.setRayon(...);
   REQUIRE(c2.getRayon()   == "..."  );
   REQUIRE(c2.toString()   == ".....");
   REQUIRE(c2.getLargeur() == ".....");
   REQUIRE(c2.getHauteur() == ".....");  

   REQUIRE(Forme::prochainId() == (compteur+2) ); 
}
TEST(Forme, Cercle) {
   int compteur = Forme::prochainId();
   Cercle c1;
   Cercle c2(...); 
   
   EXPECT_EQ(c1.toString(), ".....");
   EXPECT_EQ(c2.toString(), ".....");

   c2.setRayon(...);
   EXPECT_EQ(c2.getRayon(), );
   EXPECT_EQ(c2.toString(), ".....");
   EXPECT_EQ(c2.getLargeur(), ".....");
   EXPECT_EQ(c2.getHauteur(), ".....");  

   EXPECT_EQ(Forme::prochainId(), compteur+2); 
}

On pourra ensuite vérifier le polymorphisme fort (les tests sont à adapter en fonction de ce que renvoient les méthodes toString())

TEST_CASE("Polymorphisme", "[Forme]") {
   Forme * f1 = new Cercle;
   Forme * f2 = new Rectangle;

   REQUIRE(f1->toString() == ".....");
   REQUIRE(f2->toString() == ".....");

   delete f1;
   delete f2;
}
TEST(Forme, Polymorphisme) {
   Forme * f1 = new Cercle;
   Forme * f2 = new Rectangle;

   EXPECT_EQ(f1->toString(), ".....");
   EXPECT_EQ(f2->toString(), ".....");

   delete f1;
   delete f2;
}

La classe Forme devra être abstraite et vous devrez choisir quelles méthodes sont virtuelles (pures ou non). Dès que la classe aura été modifiée pour être abstraite, les premiers tests avec instanciation ne seront plus utilisables (le compilateur n'acceptera plus leur compilation)

Classe Liste

Le code de la classe Liste est maintenant bien plus facile à écrire !

Vous allez transformer la classe Liste en une classe Groupe. On aurait pu faire de l'héritage multiple mais on en l'a pas encore vu en cours, n'est-ce pas ?

Un groupe est une forme. Dans cette classe, le point, la largeur et la hauteur sont calculés comme étant ceux de la boîte englobante et mis à jour lors des opérations d'ajout et de suppression des formes au groupe. Le polymorphisme et l'upcasting permettent de vérifier que la relation d'héritage est correcte.

Le groupe peut servir à stocker une scène à afficher.

Aller plus loin...

Vous pouvez coder une interface texte qui répondra aux messages suivants

$ creer cercle 10 10 30 30
=> 1
$ creer cercle 40 40 20
=> 2
$ creer rectangle 30 30 15 15
=> 3
$ afficher
CERCLE 1 10 10 30 30
CERCLE 2 20 20 40 40 
RECTANGLE 3 30 30 15 15
$ creer groupe
=> 4
$ ajouter 4 1 
$ ajouter 4 2
$ afficher
GROUPE 4 10 10 40 40
  CERCLE 1 10 10 30 30
  CERCLE 2 20 20 40 40 
RECTANGLE 3 30 30 15 15
$ contient 3 40 40 
true

Une pincée de profilage

Voici un petit programme à analyser avec un profiler. On pourra par exemple compter le nombre de fois que certaines méthodes sont appelées

#include<iostream>
#include<cmath>
#include<cstdlib>

class Element {
   double x, y;
   bool ajour;
   double distance;

   void calculerDistance();

 public:
   Element();
   Element(double, double);  
   void setX(double);
   void setY(double);
   double getDistance();
   double getDistance2();
};

Element::Element():x(.0), y(.0), ajour(true), distance(.0){
} 

Element::Element(double px, double py):x(px), y(py), ajour(false), distance(.0) {
}

void Element::calculerDistance() {
  distance = sqrt(x*x+y*y);
  ajour = true;
}

void Element::setX(double px) {
  x = px;
  ajour = false;
}

void Element::setY(double py) {
  y = py;
  ajour = false;
}

double Element::getDistance() {
   if (!ajour)
    calculerDistance();
   return distance;
} 

double Element::getDistance2() {
   calculerDistance();
   return distance;
} 

int main(int, char**) {
    Element e(10.0, 100.0);

    for(int i=0;i<100000; ++i) {

       if (!(random() % 7) ) {
          e.setX((double)rand());
          e.setY((double)rand());
       }
       std::cout << e.getDistance() << " "; 
       std::cout << e.getDistance2() << std::endl; 
    }
  return 0;
} 

Rappel : gprof

Pour profiler avec gprof, il suffit :

  1. de compiler le programme avec l'option -pg.
  2. de lancer l'exécutable : cela génère un fichier gprof.out
  3. d'analyser le fichier en le donnant à gprof.

Note : un programme compilé avec l'option -pg est beaucoup plus long à s'exécuter.

Valgrind

Nous pouvons également utiliser ce couteau suisse de la programmation : valgrind. Il suffit pour cela de choisir un autre outil que celui par défaut (memcheck).

  1. Compiler avec l'option -g
  2. Exécuter valgrind --tool=callgrind --dump-instr=yes executable
  3. Analyser les résultats avec kcachegrind (installé sur etud)

Note : valgrind utilise une simulation de processeur, l'exécution peut donc être 50 fois plus longue que le programme original, l'avantage est que le temps de calcul utilisateur ne dépend pas de la charge machine