Partage
  • Partager sur Facebook
  • Partager sur Twitter

surcharge operator<< virtual ?

Sujet résolu
7 septembre 2018 à 23:22:00

Bonsoir la communauté,

j'ai du mal à passer une surchage de l'opérateu<< en mode virtual. 

class Forme {
protected:

public:
    virtual ostream& description(ostream& sortie) const {
        sortie << "Ceci est une forme" << endl;
        return sortie;
    }

};

class Cercle : public Forme {
protected:

public:

    virtual ostream& description(ostream& sortie) const {
        sortie << "Ceci est un cercle" << endl;
        return sortie;
    }

};
/* Surcharge operator << */
ostream& operator<<(ostream& sortie, const Forme& forme) {
    return forme.description(sortie);
}

ostream& operator<<(ostream& sortie, const Cercle& cercle) {
    return cercle.description(sortie);
}

int main()
{
    Forme f;
    Cercle c;
    f.description(cout);
    c.description(cout);

    Forme f2(c);
    f2.description(cout);

Donc pour les objets f et c la bonne méthode description est utilisée(celle de la classe), jusque là c'est logique. 

Mais lorsque je créé un nouvel objet f2 qui est issu de l'object c, je pourrais m'attendre à ce qu'il utilise la méthode description de la classe cercle. Mais ce n'est pas le cas et je n'arrive pas à lui spécifier le truc. Une idée? :)

  • Partager sur Facebook
  • Partager sur Twitter
8 septembre 2018 à 0:21:40

Salut,

C'est parce que l'opérateur << lui même ne peut pas être virtuel :p

Par contre, il n'y a absolument rien qui t'empêche de faire appel à un comportement virtuel à l'intérieur de ton opérateur<<, si tu manipule des éléments qui font partie d'une hiérarchie de classe.

On peut, d'ailleurs, trouver un tas de bonnes raisons à travailler de la sorte, la principale étant que les comportements (virtuels ou non) auxquels tu feras appel à l'intérieur de ton opérateur << ont "beaucoup plus de chances" de correspondre à des comportement que tu peux attendre de la part de ta classe (ou de ta classe de base) en séparant très bien le travail entre celui qui développe la classe, et celui qui utilise la classe.

En effet -- c'est peut être le point le plus épineux a assimiler quand on commence à apprendre -- la responsabilité de celui qui développe la classe est de fournir "quelque chose" qui soit utilisable, si possible, en faisant le maximum pour que ce quelque chose soit "facile à utiliser correctement et difficile à utiliser de manière incorrecte", sans commencer (et c'est sans doute le point le plus difficile à admettre) à s'inquiéter de l'usage qui sera fait de son "quelque chose".

Dans ton exemple, tu voudrais envoyer la description de ta classe à un flux de données sortant.  La bonne question à se poser est : "mais pourquoi se limiter à un flux de données sortant?"

Et la bonne réponse à cette question est : "il n'y a absolument aucune raison de se limiter de la sorte".

Pourquoi? Simplement, parce que l'on peut trouver des centaines de situations dans lesquelles nous pourrions vouloir... disposer de cette description sans forcément vouloir l'envoyer dans un flux de données sortant.

Je me doute bien que tu ne vois, à l'heure actuelle, sans doute pas encore dans quelle situation cela pourrait t'arriver.  C'est normal! tu débutes, et on ne peut pas tout savoir avant même d'avoir appris ;)

Aussi peut-être devrais tu dés à présent essayer de prendre une habitude "relativement simple"(à comprendre, à tout le moins)  qui est, quand tu es confronté à une situation "très spécifique" (comme l'envoi d'une donnée dans un flux), de faire la distinction entre ce qui est spécifique à ton besoin et ce qui  peut potentiellement être utile de "manière commune", même pour des besoins qui sont très différents du besoin sur lequel tu "planches".

Dans le cas présent, ce qui est (potentiellement) utile de "manière commune", c'est d'obtenir la description de la forme, alors que ce qui est "spécifique à ton besoin" est... d'envoyer cette description dans le flux sortant.

Tu pourrais donc modifier ta classe pour qu'elle fournisse sa description sous une forme utilisable "de manière commune", par exemple, en renvoyant -- tout simplement -- une chaine de caractères, ce qui lui donnerait une forme proche de

class Forme {
protected:
 
public:
    /* !!! nécessite l'inclusion du fichier d'en-thete <string> */
    virtual std::string description() const {
        return std::string{"Ceci est une forme"};
    }
 
};
 
class Cercle : public Forme {
protected:
 
public:
    /* Dés avant C++11, il n'était pas indispensable de
     * répéter le mot clés virtual pour les fonctions 
     * que l'on redéfinissait dans les classes dérivées
     * un simpe
    std::string description() const {
     * aurait donc suffit...
     *
     * Depuis C++11, on dispose du "post qualificateur" 
     * override, qui demande au compilateur de vérifier 
     * que l'on redéfini effectivement une fonction déclarée
     * comme virtuelle dans la classe de base
     * ainsi (éventuellement) du "post qualificateur" final
     * qui indique au compilateur que la fonction ne sera
     * plus redéfinie par la suite
     */
    std::string description() final override{
        return std::strin{"Ceci est un cercle" };
    }
 
};

Et, de cette manière, tu n'auras plus qu'à ... fournir le comportement spécifique que tu veux observer lorsque ton idée sera de transmettre ces informaitons dans un flux sortant, ce qui prendrait une forme proche de

ostream& operator<<(ostream& sortie, const Forme& forme) {
    sortie << forme.description();
    return ofs;
}

Et, le mieux de l'histoire, c'est que cela fonctionnera avec n'importe quelle classe dérivée de forme que tu pourrais envisager de créer par la suite ;)

  • Partager sur Facebook
  • Partager sur Twitter
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
8 septembre 2018 à 6:27:03

l'opérateur << comme son pendant >> doivent être implémentés sous forme de fonction libre.  

std::ostream & operator << ()(std::ostream & output,T const & t)
{
   // outputing stuff
   return output; 
}

-
Edité par int21h 8 septembre 2018 à 6:28:02

  • Partager sur Facebook
  • Partager sur Twitter
Mettre à jour le MinGW Gcc sur Code::Blocks. Du code qui n'existe pas ne contient pas de bug
8 septembre 2018 à 9:20:53

Merci pour ta réponse! Le temps de finir le petit dej et je teste ça. :)

Par contre là où j'approuve une difficulté c'est que je ne comprends pas très bien le type ostream et ses conséquences/avantage finalement.(par rapport à un string (que je maîtrise un minimum et où comprends le concept))

EDIT :

Donc en modifiant le code ça donne ça (j'ai gardé un ostream car je veux pouvoir aussi utiliser des attributs en plus de texte)

class Forme {
protected:

public:
    virtual ostream& description(ostream& sortie) const {
        sortie << "Ceci est une forme" << endl;
        return sortie;
    }

};

class Cercle : public Forme {
protected:

public:

    ostream& description(ostream& sortie) const override {
        sortie << "Ceci est un cercle" << endl;
        return sortie;
    }

};


/* Surcharge operator << */
ostream& operator<<(ostream& sortie, const Forme& forme) {
    return forme.description(sortie);
}

int main()
{
    Forme f;
    Cercle c;
    cout << f;
    cout << c;

    Forme f2(c);
    cout << f2;


    system("pause");
    return 0;
}

f2 affiche toujours "Ceci est une forme".. Il y a un truc que je n'ai pas du comprendre..

-
Edité par Irtis 8 septembre 2018 à 11:06:23

  • Partager sur Facebook
  • Partager sur Twitter
8 septembre 2018 à 10:54:23

Pour le comportement virtuel, la question est un classique de la FAQ: https://cpp.developpez.com/faq/cpp/?page=La-surcharge-d-operateurs#Comment-surcharger-correctement-l-operateur-lt-lt-pour-afficher-des-objets-polymorphes

Ensuite, ce qu'il faut comprendre, c'est que pour pouvoir écrire: "a @ b" (@ étant un opérateur binaire quelconque, et a et b des instances respectives de A et de B), il n'y a que deux possibilités:

1- soit il existe un opérateur membre "A::operator@(B)",
2- soit un opérateur libre "operator@(A, B)"

Définir `B::operator(A)`, ne permettrait que d'écrire `b @ a`, et pas `a @ b` comme on le voulait.

Pour écrire `cout << o`, pour le cas 1-, il faudrait pouvoir écrire `ostream::operator<<(O)`. Sauf que nous n'avons pas le droit de rajouter une fonction dans std::ostream. Donc, il faut rajouter à l'extérieur.

  • Partager sur Facebook
  • Partager sur Twitter
C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
9 septembre 2018 à 22:59:09

Irtis a écrit:

Par contre là où j'approuve une difficulté c'est que je ne comprends pas très bien le type ostream et ses conséquences/avantage finalement.(par rapport à un string (que je maîtrise un minimum et où comprends le concept))

string et stream n'ont a peu prés rien à voir, il n'est donc pas étonnant que que tu ais un problème pour les relier. string est la représentation standard pour une chaîne de caractères, stream est un modèle pour représenter des flux de données (en anglais flux se traduit par stream). Tu as là deux objets qui n'ont absolument rien à voir entre eux, ils peuvent être utilisés conjointement, mais n'ont fondamentalement rien à voir. Derrière un flux, il peut se cacher beaucoup de choses, le premier exemple que les débutants voient c'est la console avec cin et cout, ça peut aussi être un fichier via la classe fstream, mais ça peut être tout à fait autre chose comme par exemple une connexion réseau...
  • Partager sur Facebook
  • Partager sur Twitter
Mettre à jour le MinGW Gcc sur Code::Blocks. Du code qui n'existe pas ne contient pas de bug
10 septembre 2018 à 9:17:30

Hello la communauté,

Merci pour votre aide (je vais aller lire quelques truc sur les streams car je commence à pas mal les utiliser sans forcement bien comprendre ce qui se cache derrière, et comme tu le dis int21 ça peut regrouper beaucoup de choses finalement)

J'ai amélioré mon code et ça marche! :)

class Forme {
public:
    virtual ostream& description(ostream& sortie) const {
        sortie << "Ceci est une forme" << endl;
        return sortie;
    }
    virtual unique_ptr<Forme> copier_ptr() {
        return unique_ptr<Forme> (new Forme(*this));
    }
};

class Cercle : public Forme {
public:
    ostream& description(ostream& sortie) const override {
        sortie << "Ceci est un cercle" << endl;
        return sortie;
    }

    unique_ptr<Forme> copier_ptr() override {
        return unique_ptr<Forme> (new Cercle(*this));
    }
};


/* Surcharge operator <<  <> */
ostream& operator<<(ostream& sortie, const Forme& forme) {
    return forme.description(sortie);
      }

int main()
{
    unique_ptr<Forme> f (new Forme());
    unique_ptr<Cercle> c (new Cercle());
    f->description(cout);
    c->description(cout);

    unique_ptr<Forme> f2;
    f2 = c->copier_ptr();
    f2->description(cout); 
    /* ou cout << *f2; pour utiliser la surcharge de l'operator<<  */
    /* En vrai c'est plus interessant d'utiliser ça avec une collection, mais bon c'était surtout pour tester!
    vector<unique_ptr<Forme>> f2;
    f2.push_back(c->copier_ptr());
    for(auto const& f : f2)
        f->description(cout);
    */

    system("pause");
    return 0;
}

C'est bien d'utiliser une méthode copier pour gérer ce genre de cas? (insérer dans un vector ou un autre ptr (d'une autre classe par exemple)) Et est-ce qu'il y a plus simple que ça?

Est-ce qu'il y a des choses à améliorer dans ce que j'ai écris? (il y en a toujours! :) )

Bonne journée.

-
Edité par Irtis 10 septembre 2018 à 9:23:30

  • Partager sur Facebook
  • Partager sur Twitter
10 septembre 2018 à 12:59:11

Hello,

Dans un premeir temps, j'ai du mal à comprendre pourquoi tu surcharges l'operateur << si c'est pour au final utiliser la fonction description() pour afficher. Quitte à surcharger l'opérateur de flux, utilise-le dans le main et sert toi de la fonction description comme te l'a indiqué @koala.

Ensuite, je sais pas trop ce que tu veux faire avec la fonction copier_ptr() mais ça me semble moche.

Enfin, n'utilises pas new pour initialiser un pointeur intelligent, il y a une fonction toute prête pour ça : std::make_unique<T>() ou std::make_shared<T>(), selon le pointeur.

#include <iostream>
#include <memory>
#include <vector>

/////////////// SHAPE
class Shape
{
public:
    virtual ~Shape() = default;

    virtual std::string description() const
    { return std::string{"I'm a Shape"}; }
};

/////////////// CIRCLE
class Circle : public Shape
{
public:
    virtual ~Circle() = default;

    virtual std::string description() const final override
    { return std::string{"I'm a Circle"}; }
};

/////////////// OVERLOAD <<
std::ostream& operator<<(std::ostream& os, const Shape& s)
{
    os << s.description();
    return os;
}

/////////////// MAIN
int main()
{
    Circle c{};

    // Avec une référence
    Shape& f1 = c;
    std::cout << f1 << '\n';

    // Avec un pointeur
    std::unique_ptr<Shape> f2 = std::make_unique<Circle>(c);
    std::cout << *f2 << '\n';

    // Dans une collection
    std::vector<std::unique_ptr<Shape>> v{};
    v.push_back(std::make_unique<Circle>(c));
    std::cout << *v[0] << '\n';

    return 0;
}

Note que le destructeur doit être virtual aussi.

Une dernière chose, évite d'utiliser using namespace std; c'est une mauvaise pratique.

-
Edité par Guit0Xx 10 septembre 2018 à 13:07:40

  • Partager sur Facebook
  • Partager sur Twitter

...

12 septembre 2018 à 10:59:18

Top! Merci pour ta réponse ça m'a beaucoup aidé!

Avec la fonction copier_ptr je cherchais à copier la valeur d'un unique ptr vers un nouveau unique_ptr pour ensuite l'insérer dans le tableau. Mais c'est plus simple de copier l'objet avec make_unique<Circle>(c)! :)

Je ne connaissais pas ces "nouvelles" fonctions de c++14. Pour l'instant je n'utilisais que c++11..

Petite question sur cette fonction justement, en faisant make_unique<Circle>(c). Je suppose que c'est le constructeur de copie par default qui est appelé et qui passe en paramètre les attribut de la classe, non?

Edit : pourquoi le destructeur (même sans aucune instruction) doit être en virtual?

-
Edité par Irtis 12 septembre 2018 à 11:13:13

  • Partager sur Facebook
  • Partager sur Twitter
12 septembre 2018 à 12:51:19

Irtis a écrit:

Petite question sur cette fonction justement, en faisant make_unique<Circle>(c). Je suppose que c'est le constructeur de copie par default qui est appelé ?

Oui, c'est le copy constructor qui est appelé dans ce cas là car "c" est une lvalue. Si on avait utilisé une rvalue,  c'est le move constructor qui aurait été appelé.

Copy (lvalue) :

Circle c{};
auto c1 = std::make_unique<Circle>(c); // copy constructor

Move (rvalue) :

auto c2 = std::make_unique<Circle>(Circle{}); // move constructor

Irtis a écrit:

pourquoi le destructeur (même sans aucune instruction) doit être en virtual?

Parce que si le destructeur n'est pas virtual, dans le cas de l'utilisation du polymorphisme avec pointeur, au moment de la destruction de l'objet on se retrouverait avec une fuite de mémoire car le destructeur de la classe dérivée ne serait pas appelé.

Un exemple sans destructeur virtual :

class Base
{
public:
    Base() { std::cout << "Base constructed" << '\n'; }
    ~Base() { std::cout << "Base destructed" << '\n'; }
};

class Derived : public Base
{
public:
    Derived() { std::cout << "Derived constructed" << '\n'; }
    ~Derived() { std::cout << "Derived destructed" << '\n'; }
};

int main()
{
    std::unique_ptr<Base> b = std::make_unique<Derived>();

    return 0;
}
// Output

Base constructed
Derived constructed
Base destructed

Ici on voit bien que le destructeur de la classe Derived n'est pas appelé, seul celui de Base l'est.

Un exemple avec destructeur virtual :

class Base
{
public:
    Base() { std::cout << "Base constructed" << '\n'; }
    virtual ~Base() { std::cout << "Base destructed" << '\n'; }
};

class Derived : public Base
{
public:
    Derived() { std::cout << "Derived constructed" << '\n'; }
    virtual ~Derived() { std::cout << "Derived destructed" << '\n'; }
};

int main()
{
    std::unique_ptr<Base> b = std::make_unique<Derived>();

    return 0;
}
// Output

Base constructed
Derived constructed
Derived destructed
Base destructed

Maintenant on voit bien que le destructeur de Derived est appelé puis celui de Base.

-
Edité par Guit0Xx 12 septembre 2018 à 12:52:16

  • Partager sur Facebook
  • Partager sur Twitter

...

14 septembre 2018 à 11:56:49

Merci. Vos explications m'ont bien aidé!

Bonne journée.

  • Partager sur Facebook
  • Partager sur Twitter