IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

Héritage multiple et polymorphisme


Sujet :

C++

  1. #1
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut Héritage multiple et polymorphisme
    hello,

    J'utilise openFrameworks mais j'ai une question orientée C++ plus que framework

    J'ai une appli qui utilise une imprimante. Cette imprimante peut être de plusieurs type :
    - standard via CUPS en appelant une commande système
    - imprimante WiFi
    - Imprimante thermique sur port série via USB en utilisant un addon (ofxThermalPrinter)

    J'ai pas mal lu à propos de l'héritage et le polymorphisme mais des subtilités m'échappent. Je ne suis pas trop familier avec ces notions ne les utilisant pas très souvent. Mais dans mon cas, cela semble recommandé !

    donc j'imagine utiliser des classes de la sorte :
    - BasePrinter class
    - PrinterCUPS class dérivée de BasePrinter
    - PrinterWIFI class dérivée de BasePrinter
    - PrinterThermal class dérivée de BasePrinter et ofxThermalPrinter

    les contraintes :
    - toutes les imprimantes ont une méthode print()
    - seules les imprimantes WiFi et Série ont une méthode close()

    Ce que je vois dans mon cas c'est que je n'aurais pas besoin de BasePrinter puisque toutes les imprimantes fonctionneront différemment. J'imagine donc que je dois utiliser une classe abstraite ?

    j'ai donc dans mon cas (cas uniquement pour ThermalPrinter)

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
       class Printer
        {
        public:    
            Printer() {};
            virtual ~Printer() {};
     
            virtual void print() = 0;
        }
    -

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       #include <printer.h>
        #include <ofxThermalPrinter.h>
     
        class ThermalPrinter : public Printer, public ofxThermalPrinter {
        public:
            ThermalPrinter();
            virtual ~ThermalPrinter();
     
            virtual void print();
     
        };
    Ais-je besoin d'utiliser des pointeurs ?
    Comment utiliser / instancier ces imprimantes ? dois-je passer par BasePrinter ou les classes dérivées ?

    toute aide serait le bienvenu !

    Merci beaucoup !

  2. #2
    Expert éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement matériel électronique
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Décembre 2015
    Messages : 1 565
    Points : 7 642
    Points
    7 642
    Par défaut
    Citation Envoyé par Marty69 Voir le message
    Ce que je vois dans mon cas c'est que je n'aurais pas besoin de BasePrinter puisque toutes les imprimantes fonctionneront différemment. J'imagine donc que je dois utiliser une classe abstraite ?
    Tout à fait.
    Ais-je besoin d'utiliser des pointeurs ?
    Dès que l'on souhaite utiliser une capacité commune, on ne peut le faire que par pointeur nu, par pointeur 'intelligent' ou par référence.

    Comment utiliser / instancier ces imprimantes ? dois-je passer par BasePrinter ou les classes dérivées ?
    On doit créer une classe dérivée et on pourra s'en servir via pointeur ou référence de type BasePrinter
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    // exemple avec pointeur intelligent
    std::shared_ptr<Printer> printer = std::make_shared<ThermalPrinter>(/*paramètres constructeur ThermalPrinter*/);
     
    printer->print();
    Mais attention, s'il existe des fonctions spécifiques à un type, on doit revenir au type dérivé pour pouvoir l'utiliser.
    Si on prend la fonction close(), où faut-il la déclarer? Pas dans BasePrinter car ThermalPrinter n'a pas cela. Il faut s'intéresser au service attendu. Exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PrinterWIFI prtrwifi();
    prtrwifi.fctSpecifique();
    effectuerTraitement( prtrwifi ); // effectuera des actions complexes mais n'utilisera que baseprinter.print()
    prtrwifi.fctSpecique();
    prtrwifi.close();
     
    // avec
    void effectuerTraitement( BasePrinter &bp ) { // ici on utilise une référence
       ...
       bp.print();
       ...
    }
    Dans ce cas, l'héritage joue pleinement son rôle.
    Mais si à un moment pendant effectuerTraitement(), on a une action dépendant du type d'imprimante, tout l'intérêt de l'héritage disparaît.

  3. #3
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut
    Merci beaucoup pour toutes ces précisions.
    Evidement je n'ai pas tout compris, mais ça va rentrer

    Dès que l'on souhaite utiliser une capacité commune, on ne peut le faire que par pointeur nu, par pointeur 'intelligent' ou par référence.
    qu'appelles tu capacité commune ?
    Aussi j'ai bien lu au fil de mes recherches que dans certains cas il faut utiliser les pointeurs (ou références). D'ou ma question. Ce que je ne saisis pas c'est pourquoi ?

    Et aussi cette info comme quoi un constructeur ne peut être virtuel mais qu'il est bon d'avoir le destructeur virtuel...

    Il faut s'intéresser au service attendu. Exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PrinterWIFI prtrwifi();
    prtrwifi.fctSpecifique();
    effectuerTraitement( prtrwifi ); // effectuera des actions complexes mais n'utilisera que baseprinter.print()
    prtrwifi.fctSpecique();
    prtrwifi.close();
     
    // avec
    void effectuerTraitement( BasePrinter &bp ) { // ici on utilise une référence
       ...
       bp.print();
       ...
    }
    Dans ce cas, l'héritage joue pleinement son rôle.
    Mais si à un moment pendant effectuerTraitement(), on a une action dépendant du type d'imprimante, tout l'intérêt de l'héritage disparaît.
    Ca y est, je suis perdu (ah ces pointeurs, c'est mystique !)

    Enfin, comment faire pour choisir le bon type d'imprimante (l'instanciation je veux dire) ?
    - faut il filtrer et appeler manuellement la bonne classe dérivée ?
    - peut on faire en sorte que la bonne classe dérivée soit appelée automatiquement, en fonction des arguments passés au constructeur par exemple ?
    - j'ai lu des choses à propos des Factory (que j'ai encore du mal à intégrer). Est-ce une façon d'appeler la bonne classe en fonction des besoins ?

    Merci encore pour ces précisions

  4. #4
    Expert confirmé
    Inscrit en
    Mars 2005
    Messages
    1 431
    Détails du profil
    Informations forums :
    Inscription : Mars 2005
    Messages : 1 431
    Points : 4 182
    Points
    4 182
    Par défaut
    Citation Envoyé par dalfab Voir le message
    Mais attention, s'il existe des fonctions spécifiques à un type, on doit revenir au type dérivé pour pouvoir l'utiliser.
    Si on prend la fonction close(), où faut-il la déclarer?
    Il est aussi envisageable de définir close comme fonction vide dans la classe de base puis de la redéfinir dans les classes dérivées qui en ont le besoin.


    Citation Envoyé par Marty69 Voir le message
    qu'appelles tu capacité commune ?
    Une capacité décrite par les données et méthodes déclarées dans la classe de base, et donc commune aux classes dérivées.


    Citation Envoyé par Marty69 Voir le message
    Aussi j'ai bien lu au fil de mes recherches que dans certains cas il faut utiliser les pointeurs (ou références). D'ou ma question. Ce que je ne saisis pas c'est pourquoi ?
    La taille du type dérivé peut être plus grande que celle du type de base, il n'est pas possible de passer par un objet dont la taille est connue à la compilation. On passe donc par des références (pointeurs ou références C++) qui permettent de s'affranchir de ces contraintes. Cela permet entre autre choses de maintenir des tableaux ou std::vector de BasePrinter * qui référencent des objets de taille différente.

    De plus, lorsque la classe de base est abstraite (c'est ton cas), il est carrément impossible de l'instancier.


    Citation Envoyé par Marty69 Voir le message
    Enfin, comment faire pour choisir le bon type d'imprimante (l'instanciation je veux dire) ?
    - faut il filtrer et appeler manuellement la bonne classe dérivée ?
    - peut on faire en sorte que la bonne classe dérivée soit appelée automatiquement, en fonction des arguments passés au constructeur par exemple ?
    - j'ai lu des choses à propos des Factory (que j'ai encore du mal à intégrer). Est-ce une façon d'appeler la bonne classe en fonction des besoins ?
    Tu ne peux pas choisir le type à la construction depuis la classe instanciée, cela doit être délégué vers l'extérieur. Selon ton besoin, ça peut aussi bien être le rôle d'une fonction déclarée static au sein de cette même interface, ou bien réalisé par une factory dédiée effectivement qui reçoit des arguments et te retourne un pointeur vers la classe de base.


    Citation Envoyé par Marty69 Voir le message
    Et aussi cette info comme quoi un constructeur ne peut être virtuel mais qu'il est bon d'avoir le destructeur virtuel...
    Si le destructeur n'est pas virtuel, les ressources spécifiques aux classes dérivées ne sont pas proprement détruites lors d'un delete ptr (ptr étant un pointeur vers la classe de base). Oui ce devrait être automatique, mais c'est au programmeur de s'en assurer. C'est une des nombreuses contraintes random de ce langage.

  5. #5
    Membre chevronné Avatar de Ehonn
    Homme Profil pro
    Étudiant
    Inscrit en
    Février 2012
    Messages
    788
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 34
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2012
    Messages : 788
    Points : 2 160
    Points
    2 160
    Par défaut
    Citation Envoyé par Matt_Houston Voir le message
    Si le destructeur n'est pas virtuel, les ressources spécifiques aux classes dérivées ne sont pas proprement détruites lors d'un delete ptr (ptr étant un pointeur vers la classe de base). Oui ce devrait être automatique, mais c'est au programmeur de s'en assurer. C'est une des nombreuses contraintes random de ce langage.
    Non, ça ne devrait pas être automatique. On ne veut pas toujours payer le coût d'un appel de fonction passant par la table virtual.
    Le C++ a une philosophie " pay for what you use ".

  6. #6
    Expert éminent sénior
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 369
    Points : 41 518
    Points
    41 518
    Par défaut
    La recommandation de Herb Sutter pour les destructeurs d'une classe destinée à être dérivée, c'est qu'ils soient soit protected et non-virtuels (ce qui empêche d'essayer de détruire l'objet via un pointeur vers la classe de base), soit publics et virtuels.
    L'inconvénient dans le premier cas, c'est que ça oblige à forcer une distinction entre "classe supposée être dérivée" et "classe supposée être instanciée directement" (c'est-à-dire d'un côté les classes abstraites (ou avec constructeur protected) que l'on n'est jamais censé instancier, de l'autre les classes concrètes "feuilles" dont on n'est pas censé dériver du tout; et cette distinction est une des "recommandations" de How to Write Unmaintainable Code, ce qui la rend suspecte à mes yeux.
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  7. #7
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut
    Wow ! plein d'infos ! ça m'a l'air très intéressant, mais je ne suis malheureusement pas assez calé pour en comprendre toutes les subtilités !
    Je m'y remet dès demain

  8. #8
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut
    So here i am !

    i have created 3 Printer derived class. I have created a static class in Printer class to instanciate each printer type.

    Printer Class .h :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include "ofMain.h"
    #include "defines.h"
    #include "thermalPrinter.h"
    #include "cupsPrinter.h"
    #include "instaxPrinter.h"
     
    class Printer
    {
    public:    
        Printer();
        virtual ~Printer();
     
        static Printer * create(int type);
     
        virtual void print() = 0;
    };
    Printer Class .cpp :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include "printer.h"
     
    Printer * Printer::create(int type) {
        switch (type) {
            case PRINTER_THERMAL:
                return new ThermalPrinter();
                break;
            case PRINTER_INSTAX:
                return new InstaxPrinter();
                break;
            case PRINTER_CUPS:
                return new CupsPrinter();
                break;
            default:
                break;
        }
    }
     
    Printer::Printer() {
     
    }
     
    Printer::~Printer() {
     
    }
     
    void Printer::print() {
     
    }
    i get errors in every return lines saying

    Cannot initialize return object of type 'Printer *' with an rvalue of type 'ThermalPrinter *'
    Cannot initialize return object of type 'Printer *' with an rvalue of type 'CupsPrinter *'
    Cannot initialize return object of type 'Printer *' with an rvalue of type 'InstaxPrinter *'
    well that sounds fair... but shouldn't the base class handle those requirements ?

  9. #9
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 469
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 469
    Points : 6 102
    Points
    6 102
    Par défaut
    Bonjour,

    A mon avis, tu as oublié d'écrire :
    class ThermalPrinter : public Printer
    {
    // code...
    };

    D'ailleurs, si tu l'avais fait, cela aurait provoqué des erreurs, car il y a un problème avec tes inclusions :
    Ce sont les entêtes des classes qui dérivent de Printer qui doivent inclure l'entête de la classe Printer, pas le contraire.
    Par contre, ton Printer.cpp peut inclure les entêtes des classes qui dérivent de Printer.

    Je conseille la lecture du lien suivant :
    http://www.cplusplus.com/forum/articles/10627/

  10. #10
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut
    Merci pour ta réponse...

    Ceci dit je dois être un peu fatigué, mais il me semble bien avoir tout ce que tu dis :
    - les classes dérivées incluent bien printer.h
    - printer.h inclue les classes dérivées (pas le .cpp)
    - mes classes dérivées sont bien mentionnées comme héritant de la classe Printer

    Nom : Capture d’e?cran 2016-09-18 a? 12.59.18.png
Affichages : 490
Taille : 272,2 Ko

    Nom : Capture d’e?cran 2016-09-18 a? 12.59.27.png
Affichages : 439
Taille : 209,3 Ko

    Nom : Capture d’e?cran 2016-09-18 a? 12.59.38.png
Affichages : 482
Taille : 202,1 Ko

    Nom : Capture d’e?cran 2016-09-18 a? 12.59.49.png
Affichages : 487
Taille : 242,7 Ko

  11. #11
    Expert éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 60
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement matériel électronique
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Décembre 2015
    Messages : 1 565
    Points : 7 642
    Points
    7 642
    Par défaut
    Citation Envoyé par Marty69 Voir le message
    - printer.h inclue les classes dérivées (pas le .cpp)
    Non, c'est l'inverse qu'il faut faire.
    printer.h doit être inclus par les entêtes des dérivées, il ne peut donc pas inclure leurs entêtes. Cela provoque une lecture incomplète des entête et est la cause de ton problème.
    Par contre le .cpp qui contient la factory doit tout inclure.

  12. #12
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut
    ok...
    J'avais tendence à croire que tout devais résider dans le .h, que les .cpp tiraient leurs besoins des headers et que les doublons, conflits etc étaient gérés par le compilateur

  13. #13
    Expert confirmé
    Inscrit en
    Mars 2005
    Messages
    1 431
    Détails du profil
    Informations forums :
    Inscription : Mars 2005
    Messages : 1 431
    Points : 4 182
    Points
    4 182
    Par défaut
    Citation Envoyé par Ehonn Voir le message
    Non, ça ne devrait pas être automatique. On ne veut pas toujours payer le coût d'un appel de fonction passant par la table virtual.
    Le C++ a une philosophie " pay for what you use ".
    Mais le coût d'un objet qui traîne en mémoire, ça on est prêt à le payer ? Arrêtons.. cite moi un cas d'utilisation justifiée d'un destructeur non virtuel dans une classe de base.

  14. #14
    Membre chevronné Avatar de Ehonn
    Homme Profil pro
    Étudiant
    Inscrit en
    Février 2012
    Messages
    788
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 34
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2012
    Messages : 788
    Points : 2 160
    Points
    2 160
    Par défaut
    Citation Envoyé par Matt_Houston Voir le message
    Mais le coût d'un objet qui traîne en mémoire, ça on est prêt à le payer ?
    ?
    (Il s'agit d'une philosophie, pas d'une règle absolue.)

    Citation Envoyé par Matt_Houston Voir le message
    Arrêtons.. cite moi un cas d'utilisation justifiée d'un destructeur non virtuel dans une classe de base.
    Lorsque tu n'utilises que la classe de base.
    Lorsque le type dynamique est le même que le type statique de la variable qui récupère la responsabilité mémoire.

    Après, je peux comprendre que le choix par défaut ne convienne pas à tout le monde.

  15. #15
    Expert confirmé
    Inscrit en
    Mars 2005
    Messages
    1 431
    Détails du profil
    Informations forums :
    Inscription : Mars 2005
    Messages : 1 431
    Points : 4 182
    Points
    4 182
    Par défaut
    La belle affaire, c'est donc justifié lorsque :

    • il n'y a pas d'héritage : MOTO ;
    • le compilateur a les moyens de s'en rendre compte.

  16. #16
    Membre chevronné Avatar de Ehonn
    Homme Profil pro
    Étudiant
    Inscrit en
    Février 2012
    Messages
    788
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 34
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2012
    Messages : 788
    Points : 2 160
    Points
    2 160
    Par défaut
    Citation Envoyé par Matt_Houston Voir le message
    La belle affaire, c'est donc justifié lorsque :

    • il n'y a pas d'héritage : MOTO ;
    • le compilateur a les moyens de s'en rendre compte.
    (Les choses évidentes ne sont pas forcément injustifiées )

    Pas tout à fait, il y a de l'héritage (polymorphisme d'inclusion) ici :
    Fonction :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    void f(base_t const & b) { b.fct(); }
    Utilisation :
    Le souci vient uniquement lorsque l'appel du destructeur doit être virtual comme dans cet exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::unique_ptr<base_t> p = std::make_unique<derived_t>();
    Et encore, pour être plus exact, il y aura un souci si derived_t ajoute des données membres qui ont un destructeur non vide (ou que ces données membres ont un destructeur non vides, etc).

    J'insiste sur fait qu'il s'agit d'un choix par défaut et que je comprend qu'il ne convienne pas à tout le monde.
    Cependant, j'en profite pour rajouter que, à titre personnel, je n'ai jamais eu besoin d'utiliser le polymorphisme d'inclusion
    et que la programmation générique m'a suffit jusqu'à présent.

  17. #17
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Salut,
    De manière générale, le respect du SRP et une approche réellement orientée sur les services attendu devrait pouvoir te tirer de beaucoup de mauvais pas.

    Ainsi, on envisage le fait qu'une imprimante doit... pouvoir imprimer un document

    Si bien que ta classe de base a de bonne chances de ressembler à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Printer{
    public:
        /* pour le respect de la sémantique d'entité */
        Printer(Printer const & ) = delete;
        Printer & operator=(Printer const &) = delete;
        /* pour pouvoir détruire n'importe quel type d'imprimante connue comme étant
         * une imprimante "sans autre précision"
         */
        virtual ~Printer() = default;
        /* Le principal service que l'on attend de la part de l'imprimante est
         * ... d'imprimer un document
         */
        void Print(Document const & ) ; 
    }
    Mais on se rend compte que l'impression d'un document peut en réalité nécessiter trois étapes distinctes :
    1. l'ouverture d'une communication
    2. l'impression en elle même et
    3. la fermeture de la communication

    Et, bien sur, chaque type spécifique d'imprimante a de bonne chance de faire ces trois actions "à sa propre sauce" (entre autre, certains peuvent ne pas avoir à ouvrir -- et donc à fermer -- la communication).

    L'idéal est alors d'utiliser le principe dit du NVI pour introduire les point de variations propres à chaque type d'imprimante, ce qui nous inciterait à transformer notre classe printer pour qu'elle prenne une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class Printer{
    public:
        /* pour le respect de la sémantique d'entité */
        Printer(Printer const & ) = delete;
        Printer & operator=(Printer const &) = delete;
        /* pour pouvoir détruire n'importe quel type d'imprimante connue comme étant
         * une imprimante "sans autre précision"
         */
        virtual ~Printer() = default;
        /* Le principal service que l'on attend de la part de l'imprimante est
         * ... d'imprimer un document
         */
        void print(Document const & doc){
            openChannel(); //ouvrir le canal de communication (quel qu'il soit)
            doPrint(doc); // l'impression proprement dite
            closeChannel(); // fermer le canal de communication une fois l'impression terminée
    private:
     
        virtual void openChannel() ;
        virtual void doPrint() = 0;
        virtual  void closeChannel();
        }
    };
     
    void Printer::openChannel(){
    }
    void Printer::closeChannel(){
    }
    quant à l'implémentation de ces trois fonctions, il y en a deux (openChannel et de closeChannel) pour lesquels on peut définir un comportement par déraut "acceptable" qui consisterait à ... ne rien faire.

    En effet, il se peut que l'on trouve une imprimante qui n'ait absolument pas besoin d'ouvrir (et donc de fermer) un canal de communication. Il serait donc "dommage" d'obliger le développeur à implémenter ces fonctions alors... qu'elles ne font rien !!!

    Par contre, il semble totalement impossible de fournir un comportement par défaut pour l'impression en elle-même. Nous devons donc déclarer cette fonction comme étant virtuelle pure

    Ensuite, nous voudrons sans doute pouvoir interroger l'imprimante sur son état qui peut être:
    • en attente d'un travail
    • en cours d'impression
    • en erreur (ex: un bourrage papier ou autre).
    • j'en oublie peut être

    Et ca, c'est aussi un service commun à "n'importe quel type d'imprimante".

    Nous pourrions donc sans doute modifier encore notre classe Printer pour lui donner une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    class Printer{
    public:
        enum Status{
            error,
            waiting,
            printing
        }
        Printer():status_{waiting}{
        }
        /* pour le respect de la sémantique d'entité */
        Printer(Printer const & ) = delete;
        Printer & operator=(Printer const &) = delete;
        /* pour pouvoir détruire n'importe quel type d'imprimante connue comme étant
         * une imprimante "sans autre précision"
         */
        virtual ~Printer() = default;
        /* Le principal service que l'on attend de la part de l'imprimante est
         * ... d'imprimer un document
         */
        void print(Document const & doc){
            switch(status_){
                case error:
                    throw std::runtime_error("please correct hardware problems first");
                case waiting :
                    status_= printing;
                    openChannel();
                    doPrint(doc);
                    closeChannel();
                    status_ = waiting;
                case printing:
                    throw std::runtime_error("please wait for current job ends");
            }
        }
        Status status() const{
            return status_;
        }
        bool isWaiting() const{
            return status_ == waiting;
        }
        bool isPrinting() const{
            return status_ == printing;
        }
        bool isInErrorState() const{
            return status_ == error;
        }
    private:
     
        virtual void openChannel() ;
        virtual void doPrint(Document const &) = 0;
        virtual  void closeChannel();
        Status status_;
    };
     
    void Printer::openChannel(){
    }
    void Printer::closeChannel(){
    }
    Bien sur, tu peut sans doute t'attendre à encore d'autres services de la part d'une "imprimante générique"... je te laisse juge desquels et de comment les mettre en oeuvre

    Mais bon, un fois que ceci est fait, tu pourras faire hériter les différents types d'imprimante de ta classe Printer, et au final, tu pourrais dresser une liste d'imprimantes sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    std::vector<std::unique_ptr<Printer>> allPrinters;
    allPrinters.emplace_back(std::make_unique <CupsPrinter>(/* paramètres nécessaires aux imprimantes CUPS */));
    allPrinters.emplace_back(std::make_unique<WiFiPrinter>(/* paramètre nécessaires */));
    Et tu pourrais, par exemple, utiliser cette liste pour lancer l'impression du même document sur "toutes les imprimantes en attente", sous une forme qui serait proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    /* soit doc est le document à imprimer */
    for(auto & p : allPrinters){
        /* on ne veut que les imprimantes en attente */
        if(p.get()->isWaiting())
            p.get()->print(doc);
    }
    Sympa, non ??

    Tu n'as même plus besoin de t'inquiéter de savoir "tout ce qui doit être fait" avant (ou après) de pouvoir imprimer ton document, car l'imprimante le sais bien mieux que toi
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  18. #18
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    @Matt_Houston: Concernant l'héritage et le destructeur non virtuelle, il y a plusieurs situations non-évidente aux premiers abords:

    - Les techniques de méta-programmation. Std::tuple utilise massivement de l'héritage et n'est pas virtuel.
    - L'optimisation mémoire avec de la composition de type vide (ebo), utilisé par unique_ptr et shared_ptr (à travers un std::tuple) et n'importe quel conteneur de la stl qui utilise un allocateur.
    - Le découpage de responsabilité pour bénéficier du RFID: std::_Vector_base, _List_base, etc (nom dépendant de l'implémentation) s'occupent de la gestion mémoire alors que les classes filles (std::vector, std::list) s'occupent de l'initialisation de la mémoire. Dans ce genre de cas, c'est un héritage privé, au pire protégé.
    - CRTP ou un système de mixins.
    - Pour stocker un type uniforme dans une collection en sachant exactement le type réel utilisé (un pattern visiteur par exemple).

  19. #19
    Membre expert
    Profil pro
    Inscrit en
    Mars 2007
    Messages
    1 415
    Détails du profil
    Informations personnelles :
    Localisation : France, Paris (Île de France)

    Informations forums :
    Inscription : Mars 2007
    Messages : 1 415
    Points : 3 156
    Points
    3 156
    Par défaut
    Hello tous

    Pour continuer sur la lancée de Koala, plutôt que d'utiliser un pattern NVI, j'aurais plutôt fait du RAII jusqu'au bout. C'est à dire, la classe Printer n'a qu'une seule fonction membre createPrintTask qui renvoie std::unique_ptr<PrinterTask>. Cet objet ouvre la communication dans son constructeur, propose ce qu'il faut pour imprimer (fonction membre print ?) et ferme la communication dans son destructeur. De cette manière, on utilise vraiment l'aspect acquisition de ressource propre au RAII et on peut se passer de lister les particularités des imprimantes dans l'interface (même si c'est en privé, ça reste un couplage).

    Ou alors on encapsule carrément l'ouverture et la fermeture des canaux dans les constructeurs/destructeurs des imprimantes. Là il faut connaître le context plus précisément pour faire le bon choix.

    Ca reste du chipotage, les deux solutions fonctionnent.
    Find me on github

  20. #20
    Nouveau membre du Club
    Inscrit en
    Octobre 2013
    Messages
    35
    Détails du profil
    Informations forums :
    Inscription : Octobre 2013
    Messages : 35
    Points : 25
    Points
    25
    Par défaut
    Bon j'ai décroché depuis longtemps hein ! mais je suis la conversation avec grand intérêt.
    Merci Koala pour cette belle démonstration

    J'en suis encore à débugguer parce que évidement quand on manipule (mal) les pointeurs, on se mange des vilains BAD_ACCESS !!! (enfin moi en tout cas !)

Discussions similaires

  1. composants C++ Builder et héritage multiple
    Par vedrfolnir dans le forum C++Builder
    Réponses: 2
    Dernier message: 12/10/2005, 10h04
  2. [heritage][conception]héritage multiple en java!
    Par soulhouf dans le forum Langage
    Réponses: 9
    Dernier message: 25/08/2005, 20h03
  3. L'héritage multiple est-il possible en Delphi ?
    Par SchpatziBreizh dans le forum Langage
    Réponses: 8
    Dernier message: 30/06/2005, 11h30
  4. utilisez vous l'héritage multiple ?
    Par vodosiossbaas dans le forum C++
    Réponses: 8
    Dernier message: 13/06/2005, 20h25
  5. [XML Schemas]héritage multiple
    Par nicolas_jf dans le forum XML/XSL et SOAP
    Réponses: 2
    Dernier message: 10/06/2003, 12h55

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo