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
ISIMA
  • twitter
  • linkedin
  • viadeo

[C++] TP 4

Read in English

Le TP de la semaine dernière était très dense. Terminez le TP précédent avant de faire celui-ci

Date de première publication : 2013/10/03

Petites vérifications

Avez-vous vérifié les choses suivantes ?

void test1(Bavarde b) {
  std::cout << "appel de fonction avec parametre objet et copie";
}
Bavarde test2a() {
  std::cout << "appel de fonction avec retour";
  return Bavarde(); // creation d'un objet local
} // plus de copie - voir ZZ3
Bavarde test2b() {
  Bavarde b; // creation d'un objet local
  std::cout << "appel de fonction avec retour";
  return b; 
} // plus de copie - ZZ3 
void test3(Bavarde& b) {
  std::cout << "appel de fonction avec référence ";
}
void test4(Bavarde *b) {
  std::cout << "appel de fonction avec un pointeur sur un objet";
}

Troncature de type

Vérifier ce qu'est la troncature de type et comment on l'évite ...

void afficher1(Forme f) {
   f.afficher();
}

void afficher2(Forme &f) {
   f.afficher();
}

void afficher3(Forme * f) {
   f->afficher();
}

int main(int, char**) {
   Cercle cercle;
   
   afficher1(cercle);
   afficher2(cercle);
   afficher3(&cercle);
   
   return 0;
}

Fil rouge ...

La base des deux exercices est clonable ici :

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

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 les machines LINUX)

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