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

Langage C++ Discussion :

Classe générique et erreur "request for member [..] which is of non-class type"


Sujet :

Langage C++

  1. #1
    Membre à l'essai
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    24
    Détails du profil
    Informations personnelles :
    Âge : 35
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 24
    Points : 18
    Points
    18
    Par défaut Classe générique et erreur "request for member [..] which is of non-class type"
    Bonjour,

    Environnement :
    OS : Mac OS X 10.6.4
    IDE : Xcode 3.2
    Compilateur : GCC 4.2

    Je suis en train de me remettre au C++ après avoir eu un très léger apprentissage l'année dernière. Pour cela, je m'entraîne en implémentant les structures de données usuelles. J'ai donc commencé par la pile.

    Pour cela j'utilise donc une classe générique (template de classe) :

    Stack.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
    17
    18
    19
    20
    21
    22
    23
    24
     
    #ifndef STACK_H_
    #define STACK_H_
     
    #include <iostream>
     
    template<typename T>
    class Stack {
    public: 
        Stack();
        virtual ~Stack();
     
        void push(const T);
        T pop();
        bool isEmpty() const;
        int size() const;
     
    private:
        T* _data;
        int _size;
        static void _arrayCopy(T*, T*, int);
    };
     
    #endif /* STACK_H_ */
    Stack.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
    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
     
    #include "Stack.h"
     
    using namespace std;
     
    template<typename T>
    Stack<T>::Stack() : _size(0) {
        _data = new T[0];
    }
     
    template<typename T>
    Stack<T>::~Stack() {
        delete _data;
    }
     
    template<typename T>
    void Stack<T>::push(const T element) {
        T* oldData = _data;
     
        _data = new T[++_size];
        Stack::_arrayCopy(oldData, _data, _size);
        delete oldData;
    }
     
    template<typename T>
    T Stack<T>::pop() {
     
        T element = _data[_size - 1];
     
        T* oldData = _data;
     
        _data = new T[--_size];
        Stack::_arrayCopy(oldData, _data, _size);
        delete oldData;
     
        return element;
    }
     
    template<typename T>
    bool Stack<T>::isEmpty() const {
        return _size == 0;
    }
     
    template<typename T>
    int Stack<T>::size() const {
        return _size;
    }
     
    template<typename T>
    void Stack<T>::_arrayCopy(T* source, T* destination, int copyCount) {
        for (int current = 0; current < copyCount; current++) {
            destination[current] = source[current];
        }
     
    }
    Afin de tester ma classe j'ai tout simplement commencé par l'instancier et j'appelle ensuite une méthode :

    main.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
     
    #include <iostream>
    #include "Stack.h"
     
     
    using namespace std;
     
    int main () {
     
        Stack<int> pile();
        pile.isEmpty();
     
     
        return 0;
    }
    Mon problème est que mon compilateur ne semble pas reconnaître pile comme étant un objet de ma classe Stack<T> (en l'occurence, un objet de la classe Stack<int>).

    Voici l'erreur retournée :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    
    /***/***/Workspace/containers/main.cpp:10:0
    /***/***/Workspace/containers/main.cpp:10:
    error: request for member 'isEmpty' in 'pile', which is of non-class type 'Stack<int> ()()'
    
    Ce qui me trouble dans ce message d'erreur, c'est que le compilateur mentionne une classe Stack<int> ()() (notez les deux paires de parenthèses).

    Étant donné que je manipule les classes génériques pour la première fois en C++, il est possible que mon erreur soit assez simple.

  2. #2
    Rédacteur

    Avatar de Davidbrcz
    Homme Profil pro
    Ing Supaéro - Doctorant ONERA
    Inscrit en
    Juin 2006
    Messages
    2 307
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : Suisse

    Informations professionnelles :
    Activité : Ing Supaéro - Doctorant ONERA

    Informations forums :
    Inscription : Juin 2006
    Messages : 2 307
    Points : 4 732
    Points
    4 732
    Par défaut
    est le prototype d'une fonction nommée pile ne prenant aucun argument et renvoyant un objet du type Stack<int>.
    La bonne syntaxe pour instancier un objet sans lui passer d'arguments via le constructeur est
    . Quelqu'un sait si y'a une entré dans la FAQ pour ce problème ?


    Sinon, ta classe gère un pointeur, il font donc définir constructeur de copie et operator= (cf FAQ, tout ca ....) Et pas de template dans un fichier .cpp sinon tu vas avoir des problèmes avec l'édition des liens un jour (cf FAQ again ...)
    "Never use brute force in fighting an exponential." (Andrei Alexandrescu)

    Mes articles dont Conseils divers sur le C++
    Une très bonne doc sur le C++ (en) Why linux is better (fr)

  3. #3
    Membre à l'essai
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    24
    Détails du profil
    Informations personnelles :
    Âge : 35
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 24
    Points : 18
    Points
    18
    Par défaut
    OK pour l'appel du constructeur par défaut.

    Pour le problème de l'édition de liens causé par la définition des templates dans le .cpp, il s'est en effet produit. J'ai donc suivi le conseil de la FAQ (que je me souvient avoir lu hier soir en plus ).

    Mais le C++ me semble de plus en plus étrange :
    • Ne pas pouvoir définir des templates dans le .cpp
    • L'utilisation du terme méthode virtuelle pure pour ce que j'appelle une méthode abstraite
    • L'utilisation des méthodes virtuelles pour pouvoir utiliser le polymorphisme (j'ai l'impression que le mot-clé virtual ne sert dans ce cas qu'à palier à un problème dans le langage)


    Pour la surcharge des opérateurs, je suis en train de m'y atteler.

    Merci beaucoup.

  4. #4
    Membre expérimenté
    Homme Profil pro
    Chercheur
    Inscrit en
    Mars 2010
    Messages
    1 218
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Chercheur

    Informations forums :
    Inscription : Mars 2010
    Messages : 1 218
    Points : 1 685
    Points
    1 685
    Par défaut
    Bonjour,

    Quelqu'un sait si y'a une entré dans la FAQ pour ce problème ?
    Peut-être celle-ci?

    Mais le C++ me semble de plus en plus étrange :
    Ne pas pouvoir définir des templates dans le .cpp
    Il y a débat (voir la faq).
    Est-ce que quelqu'un sait si le mot-clé export est entré dans la norme?

    L'utilisation du terme méthode virtuelle pure pour ce que j'appelle une méthode abstraite
    L'utilisation des méthodes virtuelles pour pouvoir utiliser le polymorphisme (j'ai l'impression que le mot-clé virtual ne sert dans ce cas qu'à palier à un problème dans le langage)
    Je n'ai jamais entendu parlé de "méthode abstraite".
    Tu as entendu ça où?
    Le terme "classe abstraite" est par contre employé en C++ comme ailleurs (ex:Java).

    L'utilisation des méthodes virtuelles pour pouvoir utiliser le polymorphisme (j'ai l'impression que le mot-clé virtual ne sert dans ce cas qu'à palier à un problème dans le langage)
    On peut aussi le voir comme une plus grande liberté de conception : par exemple pour la gestion de la destruction en présence d'héritage (cf. faq).

  5. #5
    Rédacteur

    Avatar de Davidbrcz
    Homme Profil pro
    Ing Supaéro - Doctorant ONERA
    Inscrit en
    Juin 2006
    Messages
    2 307
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : Suisse

    Informations professionnelles :
    Activité : Ing Supaéro - Doctorant ONERA

    Informations forums :
    Inscription : Juin 2006
    Messages : 2 307
    Points : 4 732
    Points
    4 732
    Par défaut
    Ne pas pouvoir définir des templates dans le .cpp
    Le compilo a besoin du corps des fonctions quand il instancie une classe template => faut tout mettre dans les header

    L'utilisation du terme méthode virtuelle pure pour ce que j'appelle une méthode abstraite
    méthode est assez peu utilisé dans le monde C++ien (sauf chez Stroustrup, qui lui faire correspondre la notion de fonctino membre virtuelle). On préfère parler de fonction qui peut être libre ou membre, ces dernières pouvant être qualifiées de constantes ou non, de virtuelle et même de virtuelle pure. A noter que ces derniers concepts sont orthogonaux. Une fonction membre constante virtuelle pure est tout à fait correct.
    L'utilisation des méthodes virtuelles pour pouvoir utiliser le polymorphisme (j'ai l'impression que le mot-clé virtual ne sert dans ce cas qu'à palier à un problème dans le langage)
    Tu aurais pas fait du Java (brr, j'en ai des frissons rien qu'a écrire le mot) avant ? Sinon cf ceci et un débat qu'il y a eu sur le forum C++.



    Est-ce que quelqu'un sait si le mot-clé export est entré dans la norme?
    Export a toujours été dans la norme C++03, il n'a juste jamais été supporté par la majorité des compilateurs. Je ne sais pas s'il est passé deprecated en C++0x.
    "Never use brute force in fighting an exponential." (Andrei Alexandrescu)

    Mes articles dont Conseils divers sur le C++
    Une très bonne doc sur le C++ (en) Why linux is better (fr)

  6. #6
    Membre à l'essai
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    24
    Détails du profil
    Informations personnelles :
    Âge : 35
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 24
    Points : 18
    Points
    18
    Par défaut
    Citation Envoyé par Aleph69 Voir le message
    Bonjour,
    Je n'ai jamais entendu parlé de "méthode abstraite".
    Tu as entendu ça où?
    Le terme "classe abstraite" est par contre employé en C++ comme ailleurs (ex:Java).
    Une méthode abstraite, en Java, est une méthode qui n'a pas de corps et dont la signature doit être précédée de la clause abstract. Elle doit être définie dans la classe fille.
    Une classe qui possède au moins une méthode abstraite est une classe abstraite et sa déclaration doit elle aussi être précédée de la clause abstract.
    C'est, à mon sens, la définition d'une méthode (fonction membre à ce que je viens de lire) virtuelle pure.

    Citation Envoyé par Davidbrcz Voir le message
    Tu aurais pas fait du Java (brr, j'en ai des frissons rien qu'a écrire le mot) avant ? Sinon cf ceci et un débat qu'il y a eu sur le forum C++.
    En effet, j'ai appris la POO avec le Java. J'ai ensuite eu droit à quelques cours de C++, mais pas suffisamment à mon goût.
    J'ai commencé à lire la FAQ (écrite par le créateur de C++ ?). Et j'avoue qu'elle est très utile pour comprendre les décisions prises par rapport à cette utilisation de la clause virtual.
    Cela doit sûrement permettre plus de flexibilité (comme l'a écrit Aleph69).

    Le C++ me paraît beaucoup plus complexe que le Java (dans le bon sens du terme, il à l'air de permettre de maîtriser le comportement des objects de manière beaucoup plus fine). Mais le Java me paraît justement plus adapté pour approcher les premiers concepts de la POO (je pense notamment à certains collègues d'IUT, qui auraient surement décroché si on les avait attaqué directement avec tous les concepts de C++).

    Merci pour toutes ces informations.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,
    Ne pas pouvoir définir des templates dans le .cpp
    Pour être précis et complet:

    Il faut comprendre que le but des template est de se dire "je ne sais pas ce que je manipule, mais je sais comment je le manipule".

    Il s'en suit que l'on ignore, jusqu'à ce que l'on décide d'utiliser la fonction (ou la classe) avec une donnée d'un type précis quelle taille cette donnée va réellement prendre en mémoire.

    Le compilateur ne peut donc créer le jeu d'instructions que le processeur devra effectuer qu'une fois qu'il (le compilateur) sait... quel type de donnée est manipulé, et qu'il en connait donc la taille, et il aura donc besoin du code source de la fonction pour chaque appel de celle-ci

    Or, la "bonne pratique" qui consiste à ne pas inclure un fichier d'implémentation (*.cpp) avec une directive include est de stricte application, pour éviter d'avoir des problèmes à l'édition de liens (au moment où l'on regroupe les différents fichiers "objets" en un exécutable/ une bibliothèque), seuls l'inclusion de fichiers d'en-tête étant suggérée.

    En effet, un fichier d'en-tête a tendance à se propager "comme la peste": dés qu'il est inclus dans un autre fichier d'en-tête, il sera, par le jeu des inclusions en cascades, inclus dans tout fichier qui inclus le fichier d'en-tête qui l'inclus et ce, de manière directe ou indirecte.

    Comme c'est, en définitive, ce que l'on souhaite pour les fonctions (membres de classe ou de structures) template, il faut bien... définir les fonctions dans le fichier d'en-tête

    Une alternative peut consister à utiliser une autre extension pour la définition de ces fonctions, quelle qu'elle soit, pou autant que ce ne soit pas une extension connue pour représenter un type de fichier particulier.

    La directive #include n'est en effet absolument pas difficile quant au type de fichier qu'elle copie .

    Si tu tiens à séparer la déclaration des fonctions de leurs implémentation, tu peux parfaitement mettre l'implémentation dans un fichier portant l'extension *.impl (pour "implementation") ou tcc (pour "template code c++") ou ... tout ce que tu veux finalement

    Mais cela ne t'empêchera pas de devoir inclure ce fichier supplémentaire chaque fois que tu voudra... faire appel à une des fonctions déclarée dans le le fichier d'en-tête
    L'utilisation du terme méthode virtuelle pure pour ce que j'appelle une méthode abstraite
    Les termes, tu sais, c'est principalement fonction des conventions utilisées

    En C++, on fait une différence entre fonction (membre) et méthode parce que toute fonction membre n'est pas forcément virtuelle (susceptible d'être redéfinie dans une classe dérivée) contrairement à java par exemple où toute fonction est réputée virtuelle jusqu'à preuve du contraire (comprend: jusqu'à ce que le développeur dise explicitement qu'elle ne sera plus redéfinie avec le mot clé final).

    Il est donc "utile" de pouvoir déterminer si une fonction est virtuelle ou non simplement grâce au terme utilisé:
    • le terme fonction (membre) représente une fonction non virtuelle
    • le terme méthode est utilisé exclusivement pour représenter une fonction membre virtuelle
    Mais comme il n'est pas beaucoup plus long de parler de "fonction virtuelle" plutôt que de "méthode", les deux termes sont parfaitement interchangeable.

    La notion de abstraite Vs pure vient d'un raisonnement à peut près identique, et de la sémantique des mots

    Abstrait signifie "qui n'a pas (ou ne peut pas) avoir d'existence concrète". En programmation, on dirait d'une classe abstraite que l'on ne peut pas en créer une instance (un objet du type de la classe).

    Si l'on parle de fonction virtuelle pure au lieu de méhtode abstraite, c'est parce qu'une fonction virtuelle pure est bel et bien (et avant tout) une fonction qui peut (pire, qui doit) être (re)définie dans les classes dérivées.

    Simplement, et je répond à ton troisième point,
    L'utilisation des méthodes virtuelles pour pouvoir utiliser le polymorphisme (j'ai l'impression que le mot-clé virtual ne sert dans ce cas qu'à palier à un problème dans le langage)
    il n'a pas été possible de définir un comportement cohérent de base pour la fonction, sans doute parce que la classe de base ne dispose pas des informations nécessaires ou parce qu'il est impossible de trouver un comportement minimum commun à toutes les classes dérivées.

    Le mot clé virtual indique donc au compilateur que la fonction est destinée à être redéfinie pour les classes dérivées, mais on lui indique qu'il ne doit pas chercher l'implémentation de cette fonction pour la classe de base grâce au =0 qui suit la déclaration et qui rend la fonction virutelle... pure.

    Imaginons, simplement, que tu veuilles créer une hiérarchie de véhicules disposant de différents type de moteurs (essence, électriques, atomiques, ...).

    La classe de base serait un véhicule, et tout véhicule peut démarrer (comprend: lancer le moteur), mais chaque type de moteur va démarrer de manière différente:
    • un moteur à explosion utilisera un démarreur pour faire ses premiers tours
    • un moteur électrique nécessite simplement que l'on ferme le circuit électrique
    • un moteur nucléaire nécessite (je n'y connais rien dans le fonctionnement des réacteurs nucléaires, hein) sans doute que l'on introduise le composant radio actif dans une chambre particulière.
    • ...
    Quel comportement cohérent voudrais tu donc pouvoir définir pour un véhicule dont... tu ignores le type de moteur qu'il utilise

    La seule solution qui te reste en C++, c'est de dire, pour la classe Vehicule au compilateur "attention, la fonction démarrer existe bel et bien, et elle sera (re)définie pour toutes les classes dérivées concrètes, mais, actuellement, je ne dispose pas des informations pour te dire quoi faire".

    Au final, ta classe véhicule ressemblera à
    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
    class Vehicule
    {
        public:
        virtual    // la fonction est redéfinie pour les classes dérivées
        bool       // la fonction renvoie une valeur de réussite ou d'échec
        demarrer() // on ne parle pas de la fonction "accélérer"
        = 0 ;      // je n'ai pas de comportement à te donner pour cette fonction
                   // tu ne dois donc pas perdre ton temps à chercher son 
                   // implémentation pour la classe véhicule
        /* la suite ... */
    };
    class VehiculeExplosion : public Vehicule
    {
        public:
            /* ce qu'il faut, dont */
            virtual bool demarrer(); // ici, je sais ce que tu devra faire
    };
    class VehiculeElectrique : public Vehicule
    {
        public:
            /* ce qu'il faut, dont */
            virtual bool demarrer(); // ici, je sais ce que tu devra faire
    };
    class VehiculeNucleaire : public Vehicule
    {
        public:
            /* ce qu'il faut, dont */
            virtual bool demarrer(); // ici, je sais ce que tu devra faire
    };
    En retour, le compilateur qui a "horreur du vide" te dira "très bien, tu ne me donne pas le comportement à utiliser pour la classe Vehicule, mais, en retour, je t'interdit de créer une instance de véhicule (sans plus de précison), car autrement, rien ne t'empêchera d'essayer d'invoquer la fonction démarrer et mon collègue (l'éditeur de liens) ne saura plus à quel saint se vouer".
    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

  8. #8
    Rédacteur

    Avatar de Davidbrcz
    Homme Profil pro
    Ing Supaéro - Doctorant ONERA
    Inscrit en
    Juin 2006
    Messages
    2 307
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : Suisse

    Informations professionnelles :
    Activité : Ing Supaéro - Doctorant ONERA

    Informations forums :
    Inscription : Juin 2006
    Messages : 2 307
    Points : 4 732
    Points
    4 732
    Par défaut
    Après un mail pour retrouver les références (merci luc !) , je signe !
    Ta fonction pop est mauvaise ! Il faut séparer l'accès à l'élément en haut de la pile et sa suppression. Pourquoi ?

    Imagine le code suivant:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    void f(Stack<type>& s)
    {
      type mt;
    // ...  
      mt = s.pop();
    }
    Que se passe t'il si l'opérateur d'affectation de type lance une exception ? L'élément n'est pas affecté dans mt mais il est perdu pour autant ! Plutôt embetant !

    Pour y remédier, il faut séparer l'accès (par une fonction top par exemple) de la suppression (une autre fonction pop).

    Sauf qu'avec cette version, le code n'est plus thread-safe !
    Exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    void f(stack<type>& s)
    {
      s.pop();                   // #1
      my_type mt(s.top);  // #2
    }
    Si un thread A appelle f et s'arrête après l'opération #1 puis qu'un thread 2 appelle à son tour f, un élément est perdu. Quand le thread A reprendra, ce que top renvera ne sera pas à quoi le programmeur se serait attendu. Pour éviter ca ? il faut locker.

    (le code et la démarche a été honteusement récupérée d'ici ). Plus de détails avec "c++ stack pop exception copy" dans google.
    "Never use brute force in fighting an exponential." (Andrei Alexandrescu)

    Mes articles dont Conseils divers sur le C++
    Une très bonne doc sur le C++ (en) Why linux is better (fr)

  9. #9
    Membre à l'essai
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    24
    Détails du profil
    Informations personnelles :
    Âge : 35
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 24
    Points : 18
    Points
    18
    Par défaut
    Citation Envoyé par Davidbrcz Voir le message
    Imagine le code suivant:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    void f(Stack<type>& s)
    {
      type mt;
    // ...  
      mt = s.pop();
    }
    Que se passe t'il si l'opérateur d'affectation de type lance une exception ? L'élément n'est pas affecté dans mt mais il est perdu pour autant ! Plutôt embetant !
    En effet, l'élément sera perdu.

    Il ne s'agit pour moi que d'un exercice pour assimiler les concepts du C++. Je n'ai fait que reprendre l'interface basique d'une pile et j'ai ensuite essayé de l'implémenter.

    Mais ton commentaire m'intéresse. Car je pense (peut être que je me trompe ), que le problème que tu mets en avant ne doit pas être traité par cet objet (Stack).
    En effet, le seul intérêt de la pile que je suis en train d'implémenter est d'être un simple conteneur LIFO.
    Ma fonction membre pop() doit donc faire une seule chose: s'assurer que l'élément situé au sommet est retourné et qu'il est bien retiré de la pile.
    Un possible problème avec l'opérateur d'affectation du type contenu ne concerne donc pas ma pile, qui elle, a fait son travail.

    Après, il est bien sûr possible de définir un type de pile possédant une interface différente.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,

    A la limite, je me demande si l'utilisation d'un tableau dynamique d'objet est bel et bien intéressante si le seul but est didactique.

    Ne serait-ce que parce que, même si ce n'est que pour un court instant, le fait de créer un tableau temporaire de taille +/- 1 (selon que l'on soit dans push ou dans pop) , d'en copier le contenu, de libérer le tableau original avant de récupérer la copie demande... un temps finalement très important et demande pas mal de ressources (imagine ce que cela peut représenter de sur utilisation de la mémoire si ta pile est composée de 2 000 000 éléments d'une structure de taille importante ).

    Je ne prétend pas (loin de moi cette idée) que la solution que je vais te proposer pourra être la meilleure en toute circonstances, tant il est vrai qu'elle occasion un surcout en terme d'utilisation de mémoire lorsqu'il s'agit de gérer des "petites données" comme les types primitifs, mais, peut être pourrais tu repartir de ce qui se fait à la base, et du concept même de pile.

    En effet, une pile est, classiquement, composée de trois éléments distincts:
    1. La donnée manipulée par la pile
    2. La pile elle-même
    3. une structure permettant de relier chaque donnée à celle qui a été introduite juste avant.
    Nous pourrions dire que la donnée elle-même est de type T (pour permettre la généricité, et que la structure qui relie chaque élément à son précédent est à usage interne uniquement de la pile, car, de toutes manières, il n'est pas vraiment prudent de laisser l'utilisateur y chipoter

    La pile devrait fournir des fonctions comme push, pop, empty et (pourquoi pas) size.

    Cela nous amènerait à une classe 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
    template <typename T>
    class Stack
    {
        struct Element
        {
            T data_;
            Element * prev_;
            Element(T const & d, Element*);
        };
        public:
            Stack(); //constructeur (la pile est vide par défaut )
            ~Stack();// destructeur (veille à libérer toute la mémoire)
            void push(T const&); // ajoute un élément
            T const & top() const; // renvoie l'élément sous sa forme constante
            T & top(); // renvoie l'élément sous sa forme non constante
            void pop(); //supprime le dernier élément ajouté
            size_t size() const; // nous donne le nombre d'éléments dans la pile
            bool empty() const; // nous dis si la pile est vide
        private:
            Element * last_;
            size_t size_;  
    };
    Le constructeur de la structure Element ne sera appelé que par la fonction push de Stack, le pointeur transmis correspondant au dernier élément inséré et servant pour initialiser "prev_":
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    template<typename T>
    Stack::Element::Element(T const & d, Element * p):data_(d),prev_(p){}
    Comme je viens de parler de push, autant en terminer avec elle
    Dans l'ordre elle:
    1. crée un nouvel élément en lui transmettant l'adresse pointée par last_
    2. utilise l'adresse de l'élément nouvellement créé comme "dernier élément ajouté"
    3. incrémente le compteur d'élément

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    template<typename T>
    void Stack<T>::push(T const & data)
    {
        Element * temp(data,last_); // si pas bad_alloc, aucun problème :D
        last_ = temp;
        ++size_;
    }
    Vu que je viens de parler de l'insertion, autant parler du retrait d'un élément...

    La fonction pop récupère le pointeur sur l'élément précédent, détruit le dernier élément ajouté, considère que l'élément précédent devient le dernier ajouté et décrémente le compteur, pour autant qu'il y ait au minimum un élément dans la pile:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<typename T>
    void Stack::pop()
    {
        if(last_)
        {
            Element * temp =last_->prev;
            delete last_;
            last_ = temp;
            --size_;
        }
    }
    Les fonctions top (version constante et non constante) renvoient, simplement une référence (éventuellement constante) sur le champs data du dernier élément inséré (lance une exception de logique si la pile est vide):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template<typename T>
    T const & Statck<T>::top() const
    {
        if(!last_)
            trhow std::logical_error();
        return last_->data;
    }
    template<typename T>
    T& Statck<T>::top() 
    {
        if(!last_)
            trhow std::logical_error();
        return last_->data;
    }
    La fontion empty dispose de deux solutions pour nous dire si la pile est vide : vérifier s'il y a un dernier élément insérer ou... utiliser le compteur

    Allez, va pour le test sur la présence du dernier élément inséré
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    template<typename T>
    bool Stack<T>::empty() const
    {
        return last_==NULL;
    }
    La fonction size nous renvoie simplement la valeur actuelle du compteur
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    template<typename T>
    size_t Stack<T>::size() const
    {
        return size_;
    }
    Il ne nous reste enfin que le destructeur qui doit veiller à ce que les derniers éléments (s'il en reste) soient correctement détruits:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <typename T>
    Stack<T>::~Stack()
    {
        while(last_)
        {
            Element * temp=last_->prev_;
            delete last_;
            last_ = temp;
        }
    }
    A l'extrême limite, le surcoût du à l'utilisation de pointeur devient bien moins prohibitif que la nécessité de copier l'intégralité d'un tableau dés que la donnée à manipuler présente une taille supérieure à celle nécessaire pour représenter un pointeur, et, plus cette taille est grande, moins le surcoût est prohibitif
    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

  11. #11
    Rédacteur

    Avatar de Davidbrcz
    Homme Profil pro
    Ing Supaéro - Doctorant ONERA
    Inscrit en
    Juin 2006
    Messages
    2 307
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : Suisse

    Informations professionnelles :
    Activité : Ing Supaéro - Doctorant ONERA

    Informations forums :
    Inscription : Juin 2006
    Messages : 2 307
    Points : 4 732
    Points
    4 732
    Par défaut
    Koala >> Le principe de l'ADT c'est que la cuisine interne, on s'en tape, l'important est l'ensemble des contraintes que ton type vérifie (invariants, post/pré conditions pour chaque fonction membre, ...) . Pour l'ADT "pile LIFO", il y a plusieurs implémentations possibles: listes chainées, tableau dynamique, on peut même envisager une avec un tableau statique à la boost::array (dans ce cas la pile à une capacité limitée), ... Toutes sont aussi valable les unes que les autres, la sienne en particulier. C'est que Meyer explique dans Conception et Programmation orientées objets.

    Après, ce qui est discutable c'est son choix de politique au niveau de la gestion mémoire (+/-1 à chaque push/pop, c'est pas le meilleur choix, une progression géométrique aurait sans doute été mieux [pas moyen de remettre la main sur un article qui parlait de ca])

    damien.flament >>
    Il ne s'agit pour moi que d'un exercice pour assimiler les concepts du C++. Je n'ai fait que reprendre l'interface basique d'une pile et j'ai ensuite essayé de l'implémenter.
    J'en suis bien conscient et pour ce que j'en vois, c'est bien parti

    En effet, le seul intérêt de la pile que je suis en train d'implémenter est d'être un simple conteneur LIFO.
    Ma fonction membre pop() doit donc faire une seule chose: s'assurer que l'élément situé au sommet est retourné et qu'il est bien retiré de la pile.
    Un possible problème avec l'opérateur d'affectation du type contenu ne concerne donc pas ma pile, qui elle, a fait son travail.
    C'est ton problème de fournir un code exception safe (au sens d'Abraham). En cas de plantage dans une opération impliquant pop, l'état de pile est altérée alors qu'elle ne devrait pas et c'est ton soucis de remédier ca. Tu remarquera que l'adaptareur standard stack (adaptateur car il se base sur un autre conteneur pour la gestion des données) respecte ces garanties en sépérant ton pop en deux opérations (un top et un pop effectif).

    Dans la même optique, si tu tentes une allocation dynamique de mémoire et qu'elle échoue, ce n'est pas ta faute. C'est la faute à l'utilisateur qui n'a pas assez de mémoire vive ou trop de logiciels lancés ou celle de l'os qui fragmente trop la RAM que c'est la 12 lune dans le calendrier Vulcain ou what ever .... Avec assez de mauvaise fois, ca se défend. Mais il n'empèche qu'il faudra quand même gérer le problème dans ton code, que ca soit ta faute ou non.
    "Never use brute force in fighting an exponential." (Andrei Alexandrescu)

    Mes articles dont Conseils divers sur le C++
    Une très bonne doc sur le C++ (en) Why linux is better (fr)

  12. #12
    Membre expérimenté
    Homme Profil pro
    Chercheur
    Inscrit en
    Mars 2010
    Messages
    1 218
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Chercheur

    Informations forums :
    Inscription : Mars 2010
    Messages : 1 218
    Points : 1 685
    Points
    1 685
    Par défaut
    Bonsoir,

    Citation Envoyé par damien.flament Voir le message
    Une méthode abstraite, en Java, est une méthode qui n'a pas de corps et dont la signature doit être précédée de la clause abstract. Elle doit être définie dans la classe fille.
    Une classe qui possède au moins une méthode abstraite est une classe abstraite et sa déclaration doit elle aussi être précédée de la clause abstract.
    C'est, à mon sens, la définition d'une méthode (fonction membre à ce que je viens de lire) virtuelle pure.
    Ah oui d'accord.
    Il y a une certaine cohérence aux deux terminologies.
    En Java, on distingue l'héritage (mot-clé extends) de l'implémentation (mot-clé implements).
    En C++, ces deux notions sont distinguées en encapsulant l'héritage.
    Un héritage public est une extension.
    Un héritage protégé ou privé est une implémentation.

    Par contre, je ne sais pas s'il existe une terminologie consacrée pour désigner les fonctions membres virtuelles pures/méthodes abstraites dans la théorie de la programmation objet.

  13. #13
    Membre à l'essai
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    24
    Détails du profil
    Informations personnelles :
    Âge : 35
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 24
    Points : 18
    Points
    18
    Par défaut
    Citation Envoyé par Davidbrcz Voir le message
    Après, ce qui est discutable c'est son choix de politique au niveau de la gestion mémoire (+/-1 à chaque push/pop, c'est pas le meilleur choix, une progression géométrique aurait sans doute été mieux [pas moyen de remettre la main sur un article qui parlait de ca])
    Sur ce point, je suis tout à fait d'accord. Mais je voudrais juste préciser que le code que j'ai posté n'est qu'un premier "jet" et qu'il a déjà évolué (surcharge de l'opérateur <<, quelques tests avec la création et le lancement des exceptions).
    Je me rend très bien compte que ma gestion du stockage des données est totalement inadaptée à un conteneur. Notamment pour un conteneur qui ne permet d'ajouter/supprimer des éléments que un par un. Et là, même avec un petit volume de données, il suffit d'accéder à mon conteneur de manière répétée et intensive pour rapidement percevoir des problèmes de performances (une réservation d'espace mémoire à chaque accès au conteneur ! .).

    Je pensais plutôt utiliser le même principe que celui utilisé par un conteneur d'une librairie standard (je crois qu'il s'agit du vector de la std) qui lors de sa construction, réserve l'espace mémoire pour 10 élément et lorsque cet espace est totalement utilisé, augmente sa taille suivant un pas d'incrémentation. À chaque augmentation de la taille du conteneur, ce pas est lui aussi augmenté (je crois qu'il est multiplié par 2).
    Ce mécanisme semble logique, car on peut supposer que plus grand est le nombre d'éléments stocké dans le conteneur, plus on est susceptible d'en stocker à nouveau dans le futur. Et cela permet de limiter les allocations de mémoire.

    Citation Envoyé par Davidbrcz Voir le message
    C'est ton problème de fournir un code exception safe (au sens d'Abraham). En cas de plantage dans une opération impliquant pop, l'état de pile est altérée alors qu'elle ne devrait pas et c'est ton soucis de remédier ca. Tu remarquera que l'adaptareur standard stack (adaptateur car il se base sur un autre conteneur pour la gestion des données) respecte ces garanties en sépérant ton pop en deux opérations (un top et un pop effectif).

    Dans la même optique, si tu tentes une allocation dynamique de mémoire et qu'elle échoue, ce n'est pas ta faute. C'est la faute à l'utilisateur qui n'a pas assez de mémoire vive ou trop de logiciels lancés ou celle de l'os qui fragmente trop la RAM que c'est la 12 lune dans le calendrier Vulcain ou what ever .... Avec assez de mauvaise fois, ca se défend. Mais il n'empèche qu'il faudra quand même gérer le problème dans ton code, que ca soit ta faute ou non.
    Pourrais-tu m'indiquer un exemple qui provoquerai une erreur avec l'opérateur d'affectation. Par exemple dans le code que tu a utilisé précédemment :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    void f(Stack<type>& s)
    {
      type mt;
    // ...  
      mt = s.pop();
    }
    mt est un type. Ma fonction pop() retourne également un type. Je ne vois pas comment une erreur d'affectation pourrais se produire.

    Pour moi, un objet ne soit s'occuper que de ce qui le regarde (le plus possible dira-t-on). Je ne veux pas dire que je n'ai pas envie de gérer les problèmes extérieurs, mais que cela doit être fait par l'objet qui effectue la manipulation. Tout cela pour que l'architecture entre les éléments soit plus flexible, pour que les dépendances soient moins fortes. J'essaye donc d'appliquer ce principe à chaque fois.

    Citation Envoyé par Aleph69
    En Java, on distingue l'héritage (mot-clé extends) de l'implémentation (mot-clé implements).
    En C++, ces deux notions sont distinguées en encapsulant l'héritage.
    Un héritage public est une extension.
    Un héritage protégé ou privé est une implémentation.
    Merci pour cette précision. Je crois que je viens de comprendre l'utilité de la clause de visibilité lorsque que l'on déclare une classe héritée.

  14. #14
    Membre expérimenté
    Homme Profil pro
    Chercheur
    Inscrit en
    Mars 2010
    Messages
    1 218
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Chercheur

    Informations forums :
    Inscription : Mars 2010
    Messages : 1 218
    Points : 1 685
    Points
    1 685
    Par défaut
    Bonsoir,

    Citation Envoyé par damien.flament Voir le message
    Pourrais-tu m'indiquer un exemple qui provoquerai une erreur avec l'opérateur d'affectation.
    Une erreur possible avec l'opérateur d'affectation est l'auto-affectation.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Le problème majeur qu'il peut y avoir avec l'opérateur d'affectation tient dans le rique, étant donné que tu gère la mémoire de manière dynamique, que deux instances différentes de ta collection ne se partagent le même espace mémoire pour représenter les données.

    L'opérateur d'affectation est l'une des quatre fonctions que le compilateur implémente de lui-même si tu ne lui dit pas explicitement de ne pas le faire, et cette implémentation par défaut se contente de faire une affectation "membre à membre" du contenu de ta classe.

    Seulement, il faut se rappeler qu'un pointeur n'est jamais qu'une valeur numérique "un peu particulière" en cela qu'elle permet de représenter... l'adresse mémoire à laquelle la donnée se trouve effectivement, et c'est donc cette valeur qui sera copiée / affectée, avec, comme résultat, le fait que deux instances différente de ta classe essayeront d'aller "travailler" sur un espace mémoire identique.

    Cela se remarque assez facilement en demandant simplement d'afficher l'adresse mémoire représentée dans un pointeur:
    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 MaClass
    {
        public:
            MaClass(size_t nbr):ptr(new int[nbr]),nbr(nbr){}
            /* laissons le compilateur implémenter le constructeur par copie
             * l'opérateur d'affectation et le destructeur
             * !!! nous aurons une fuite mémoire, mais c'est "le moindre mal"
             * dans le cas présent :D !!!
             */
            /* par contre, on crée une fonction qui nous affiche l'adresse
             * représentée par ptr
             */
            void print() const
            {
                std::cout<<" adresse du premier element :"
                          << std::hex<<ptr<<std::endl;
            }
        private:
            int * ptr;
            size_t nbr;
    };
    int main()
    {
        MaClass obj1(10);
        MaClass copy = obj1;
        copy.print();
        obj1.print();
    }
    On constate, à l'exécution, que l'adresse représentée par ptr est strictement dientique pour les deux objets (chez moi, il s'agit, lors de mon essai, de l'adresse 0x2A6D50)

    cela signifie que, si tu modifie, en toute bonne fois, un des éléments du tableau de copy (selon mon exemple), tu modifiera également, et sans t'en rendre compte, alors que ce n'est surement pas ce à quoi tu te serais attendu, l'élément équivalent dans... obj1.

    En toute honnêteté, pour désagréable que pourrait être cette conséquence, elle est beaucoup moins grave que la deuxième conséquence dont il faut parler.

    Tu l'aura lu dans les commentaires de mon code, je n'ai pas défini le destructeur de ma classe.

    De ce fait, le compilateur fournit de lui-même une implémentation "par défaut" du destructeur qui se contente... de détruire les membres "membres à membres" dans l'ordre inverse de leur déclaration.

    Mais, encore une fois, le pointeur ne sera considéré que... comme une valeur numérique exactement au même titre que la variable membre mbr.

    Le destructeur ne pensera absolument pas à libérer la mémoire allouée à ptr, parce que le compilateur te fais confiance: étant donné que tu lui a indiqué que tu prenais la responsabilité de la gestion de la mémoire (c'est une conséquence directe de l'allocation dynamique ) il estimera que tu aura pris toutes les dispositions nécessaires pour libérer cette mémoire "en temps utiles".

    Le problème c'est... que ce n'est absolument pas le cas dans le code que je te montre en exemple

    Dans le cas très particulier que je montre ici, ce n'est pas encore un trop gros problème, car, de toutes façons, la mémoire sera automatiquement libérée par le système lorsque l'application sera quittée, mais, si tu venais à créer de nombreuses instances de MaClass (dans une boucle par exemple), cela aurait à terme (plus ou moins long) des conséquences désastreuses pouvant aller jusqu'à la nécessité de redémarrer l'ordinateur de manière brutale (avec le bouton "reset" de la tour).

    Il faudrait donc rajouter au minimum le destructeur à la classe, ce qui lui donnerait la forme 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
    class MaClass
    {
        public:
            MaClass(size_t nbr):ptr(new int[nbr]),nbr(nbr){}
            /* laissons le compilateur implémenter le constructeur par copie
             * l'opérateur d'affectation
            ~MaClass()
            {
                delete [] ptr;
            }
            /* par contre, on crée une fonction qui nous affiche l'adresse
             * représentée par ptr
             */
            void print() const
            {
                std::cout<<" adresse du premier element :"
                          << std::hex<<ptr<<std::endl;
            }
        private:
            int * ptr;
            size_t nbr;
    };
    Mais cela résoudrait un problème (celui de la fuite mémoire) et en occasionnerait deux autres :

    Je te rappelle que, dans le premier exemple, le membre ptr de obj1 et de copy fait référence à... la même adresse mémoire!!!

    Et tu dois te rappeler qu'une variable qui n'a pas été créé en ayant recours à l'allocation dynamique est automatiquement détruite lorsque l'on quitte la portée dans laquelle elle a été déclarée

    Aussi, si la fonction main venait à 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
    15
    16
    17
    int main()
    {
        MaClass obj1(10) // création de la première instance
        /* des trucs, peu importants ici */
        if(un_test_quelconque)
        {
            MaClass copy = obj1; // !!! obj1.ptr et copy.ptr font référrence
                                 // à la même adresse mémoire (1) !!!
             /* des trucs */
        } /// CRAC (2)
        // n'importe quoi
        /* !!! si j'essaye d'accéder (par une fonction membre de MaClass
         * que je ne montre pas à obj1.ptr, je vais accéder à une adresse
         * mémoire qui a déjà été libérée  : BOUM (3) !!!
         */
        return 0;
    } /// BOUM (4)
    Les commentaires t'indiquent les points dangereux.

    Entre (1) et (2), nous serons toujours confrontés au fait qu'une modification d'un des élément de copy.ptr (ou de obj1.ptr) occasionnera également la modification de l'élément correspondant de obj1.ptr (respectivement copy.ptr).

    En (2), on réunit toutes les conditions qui nous mèneront à la catastrophe, même si on n'en a pas encore conscience:

    La destruction automatique de copy appelle en effet... le destructeur qui, étant donné qu'on l'a écrit de la sorte, libérera la mémoire se trouvant à l'adresse représentée par copy.ptr.

    Le problème, c'est que cette adresse particulière est toujours référencée par obj.ptr et estimée (à tord) valide pour cet objet.

    Si nous remplaçons le commentaire qui se trouve en (3) par l'appel d'une fonction quelconque de MaClass qui tente d'utiliser l'adresse mémoire représentée par obj1.ptr, nous envoyons "tout bonnement" le processeur "dans les roses" car il va essayer d'accéder à un espace mémoire qui a déjà été libéré et qui a même peut-être déjà été ... utilisé pour autre chose

    Et si on arrive, malgré tout, en (4), nous serons face à un dernier problème: la destruction automatique de obj1 et donc... l'appel du destructeur de MaClass.

    Celui-ci invoquera de nouveau le comportement de libération dynamique de la mémoire, mais, comme la mémoire référencée par ptr a déjà été libérée (lors de la destruction de copy), il y aura tentative de "double libération de la mémoire", et l'application risque fort de "planter" lamentablement

    Il faut donc prendre des précautions pour faire en sorte que, lorsqu'il y a copie d'un objet, l'original et la copie travaillent tous les deux avec des adresses mémoire différentes.

    Pour y arriver, nous devons définir le constructeur par copie pour notre classe, ce qui nous donne quelque chose 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
    class MaClass
    {
        public:
            MaClass(size_t nbr):ptr(new int[nbr]),nbr(nbr){}
            /* laissons le compilateur implémenter l'opérateur d'affectation */
            MaClass(MaClass const & rhs):ptr(new int[rhs.nbr],nbr(nbr)
            {
                memcpy(mbr,rhs.mbr,sizeof(int)*nbr);
            }
            ~MaClass()
            {
                delete [] ptr;
            }
            /* par contre, on crée une fonction qui nous affiche l'adresse
             * représentée par ptr
             */
            void print() const
            {
                std::cout<<" adresse du premier element :"
                          << std::hex<<ptr<<std::endl;
            }
        private:
            int * ptr;
            size_t nbr;
    };
    Cela résoudra les problèmes que l'on a rencontré jusqu'ici et un code 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
    int main()
    {
        MaClass obj1(10) // création de la première instance
        /* des trucs, peu importants ici */
        if(un_test_quelconque)
        {
            MaClass copy(obj1); // copie de l'objet d'origine : aucun problème :
                                // copy.ptr et obj1.ptr font référence à des adresses
                                // mémoire différentes
             /* des trucs */
        } /// destruction de la copie (aucun problème)
        // n'importe quoi
        /* je peux travailler avec obj1 sans aucun problème
         */
        return 0;
    } // destruction automatique (sans problème) de obj1
    On croirait en avoir fini, hein

    Malheureusement, il nous reste un dernier problème auquel on n'a pas pensé jusqu'à présent :

    L'opérateur d'affectation peut parfaitement servir pour affecter une nouvelle valeur à... une variable existante, comme par exemple dans
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main()
    {
        MaClass obj1(10);
        MaClass other(15);
        /* manipulation "séparée" de obj1 et de other */
        other = obj1 // affectation à other (qui existait) du contenu de obj1
        // des trucs
        return 0;
    }
    Le problème, c'est que, other.ptr pointe vers... de la mémoire allouée de manière dynamiqe et qu si on change l'adresse vers laquelle il pointe "comme cela", sans prendre de précautions, on perdra toute référence à l'adresse vers laquelle il pointait à l'origine, et nous aurons donc une fantastique fuite mémoire

    Il faut donc veiller, lorsque l'on fait une affectation, à faire en sorte:
    1. Que l'objet de "destination" (à gauche de l'opérateur =) et l'objet "d'origine" (à droite de l'opérateur =) travaille avec des adresses mémoires différentes
    2. que la mémoire qui était allouée à l'objet de destination soit correctement libérée avant de perdre toute référence à celle-ci
    Pour cela, nous mettrons généralement en place un idôme connu sous le nom de "copy and swap".

    Le principe est *relativement* simple et suit une logique en trois points:
    1. Nous commençons par créer une copie de l'objet "d'origine" (celui qui se trouve à droite de l'opérateur d'affectation et qui est transmis (sans doute sous la forme d'une référence constante) à la fonction operator = )
    2. Nous intervertissons les membre de l'objet de destination avec ceux de la copie qu'on vient juste de créer
    3. nous renvoyons l'objet de destination modifié
    Une des solutions les plus facile pour intervertir les différents membre consiste à créer une fonction "swap" et à utiliser la fonction ... swap fournie dans l'espace de noms std par la bibliothèque standard (par inclusion du fichier d'en-tête <algorithm>.

    Au final, MaClasse ressemblera donc à quelque chose 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
    class MaClass
    {
        public:
            MaClass(size_t nbr):ptr(new int[nbr]),nbr(nbr){}
     
            MaClass(MaClass const & rhs):ptr(new int[rhs.nbr],nbr(nbr)
            {
                memcpy(mbr,rhs.mbr,sizeof(int)*nbr);
            }
            MaClass & operator=(MaClass const & rhs) // (*)
            {
                MaClass temp(rhs); // copie de l'ojbet "d'origine
                swap(*this, temp) ; // intervertir les membres de l'objet en cours
                                        // et de la copie nouvellement créée
                return *this; // renvoyer l'objet en cours
            }
            ~MaClass()
            {
                delete [] ptr;
            }
            /* par contre, on crée une fonction qui nous affiche l'adresse
             * représentée par ptr
             */
            void print() const
            {
                std::cout<<" adresse du premier element :"
                          << std::hex<<ptr<<std::endl;
            }
            /* régler l'accessibilité selon les besoins réels ;) */
            friend void swap(MaClass & c1, MaClass & c2)
            {
                std::swap(c1.ptr,c2.ptr); // intervertir les pointeurs
                std::swap(c1.nbr,c2.nbr); // intervertir les tailles
            }
        private:
            int * ptr;
            size_t nbr;
    };
    (*) Nous aurions pu décider de passer directement rhs par valeur (non constante).

    Cela aurait automatiquement créé une copie de l'objet se trouvant à droite de l'opérateur =, mais de cette manière, tout est bien clair
    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

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Aleph69 Voir le message
    Bonsoir,
    Une erreur possible avec l'opérateur d'affectation est l'auto-affectation.
    L'auto affectation est, finalement, un danger bien moins grave que les problèmes que je mes en évidence dans la réponse que je viens de poster (juste un peu trop tard ).

    Il est vrai que, en cas d'auto-affectation, tu va créer assez inutilement une copie de l'objet auto-affecté, et que l'on pourrait donc prendre des précautions pour l'éviter, mais c'est clairement un moindre mal
    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

  17. #17
    Membre expérimenté
    Homme Profil pro
    Chercheur
    Inscrit en
    Mars 2010
    Messages
    1 218
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Chercheur

    Informations forums :
    Inscription : Mars 2010
    Messages : 1 218
    Points : 1 685
    Points
    1 685
    Par défaut
    Re,

    Citation Envoyé par koala01 Voir le message
    Il est vrai que, en cas d'auto-affectation, tu va créer assez inutilement une copie de l'objet auto-affecté, et que l'on pourrait donc prendre des précautions pour l'éviter, mais c'est clairement un moindre mal
    Je pensais plutôt à ce genre de choses :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    Objet Objet::operator=(Objet const& obj)
    {
     
         delete[] this->tableau; // si (obj==*this) on detruit le tableau de obj
         this->tableau = new int[obj.taille];
         for (size_t i=0;i<obj.taille;++i)
         {
               this->tableau[i] = obj.tableau[i]; // si (obj==*this) le tableau de obj a été détruit... oups!
         }

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Aleph69 Voir le message
    Re,



    Je pensais plutôt à ce genre de choses :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    Objet Objet::operator=(Objet const& obj)
    {
     
         delete[] this->tableau; // si (obj==*this) on detruit le tableau de obj
         this->tableau = new int[obj.taille];
         for (size_t i=0;i<obj.taille;++i)
         {
               this->tableau[i] = obj.tableau[i]; // si (obj==*this) le tableau de obj a été détruit... oups!
         }
    Je me doutais bien que c'était à quelque chose du genre que tu pensais

    Mais le problème vient beaucoup plus du fait que tu n'utilise pas forcément l'idiome approprié (copy and swap) et que, même si on peut se passer de cet idiome, il y a "simplement" une faille dans la logique, dans le sens où tu essaye de libérer la mémoire alors qu'il y a encore beaucoup de raisons qui pourraient faire échouer tout le processus.

    N'oublie pas, à simple titre d'exemple, que new et new[] risquent de lancer une exception de type std::bad_alloc si, par malheur, le système ne trouve pas d'espace suffisant pour répondre à ta demande

    Tu ne peux, comme le faisait remarquer david, pas être tenu pour responsable de toutes les raisons pour lesquelles l'allocation dynamique peut échouer, mais il est, malgré tout de ta seule responsabilité de veiller à en tenir compte

    Je passe sur le fait qu'une boucle pour copier un tableau d'entiers sera sans doute beaucoup plus lent qu'un memcpy, mais, si tu ne veux absolument pas entendre parler du copy and swap, le problème de l'auto-affectation se résout de manière triviale, surtout si tu prend le risque d'échec de l'allocation dynamique en compte:
    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
    /* !!! L'opérateur = renvoie une référence !!! */
    Objet & Objet::operator=(Objet const& obj)
    {
     
         /* je crée un tableau temporaire */
         int * temp= new int[obj.taille]; // aucun problème si bad_alloc est lancé
                                          // rien de mal n'a encore été fait :D
         for (size_t i=0;i<obj.taille;++i)
         {
               temp[i] = obj.tableau[i]; // aucun problème si obj==this: on 
                                         // travaille avec un tableau temporaire :D
         }
         /* NOTA : l'utilisation de this est implicite pour les membres ;)
          */
         delete [] tableau; // on évite les fuites mémoires
         tableau = temp;   // on s'assure de ne pas perdre le tableau copié
         taille = obj.taille; // et on n'oublie pas de changer la taille
        return *this;
    }
    [EDIT]En dehors du cas où les performances doivent être optimales, l'auto-affectation n'est souvent qu'un faux problème essentiellement du à une logique mal étudiée.

    Cela nous ramène en droite ligne au débat concernant l'apprentissage de la théorie avant la pratique (auquel tu participe ) et à la nécessité travailler posément en veillant à mettre au point une logique sans faille
    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

  19. #19
    Rédacteur
    Avatar de 3DArchi
    Profil pro
    Inscrit en
    Juin 2008
    Messages
    7 634
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juin 2008
    Messages : 7 634
    Points : 13 017
    Points
    13 017
    Par défaut
    Salut,
    Citation Envoyé par damien.flament Voir le message
    Merci pour cette précision. Je crois que je viens de comprendre l'utilité de la clause de visibilité lorsque que l'on déclare une classe héritée.
    F.A.Q : Quand dois-je faire un héritage public ? protégé ? privé ?

  20. #20
    gl
    gl est déconnecté
    Rédacteur

    Homme Profil pro
    Inscrit en
    Juin 2002
    Messages
    2 165
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Isère (Rhône Alpes)

    Informations forums :
    Inscription : Juin 2002
    Messages : 2 165
    Points : 4 637
    Points
    4 637
    Par défaut
    Citation Envoyé par Aleph69 Voir le message
    En Java, on distingue l'héritage (mot-clé extends) de l'implémentation (mot-clé implements).
    En C++, ces deux notions sont distinguées en encapsulant l'héritage.
    Un héritage public est une extension.
    Un héritage protégé ou privé est une implémentation.
    Si mes souvenirs du rôle de extends et d'implements en Java sont bons, j'ai bien peur que la conversion ne soit pas aussi simple et immédiate que cela.

    En outre, l'implémentation d'une interface me semble assez souvent relevé d'un héritage public (relation EST-UN dont le but est quand même souvent de manipuler l'objet concret au travers de l'interface).

+ Répondre à la discussion
Cette discussion est résolue.
Page 1 sur 2 12 DernièreDernière

Discussions similaires

  1. Réponses: 6
    Dernier message: 25/02/2013, 16h18
  2. Réponses: 2
    Dernier message: 17/02/2013, 20h59
  3. request for member
    Par annesophiedecar dans le forum C++
    Réponses: 2
    Dernier message: 11/10/2009, 21h20
  4. [C] request for member ". . ." in
    Par Meri Nose dans le forum C
    Réponses: 11
    Dernier message: 30/01/2009, 20h03
  5. Réponses: 14
    Dernier message: 14/09/2007, 17h28

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