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 :

Trimballer un unique_ptr avec la sémantique de mouvement


Sujet :

Langage C++

  1. #1
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut Trimballer un unique_ptr avec la sémantique de mouvement
    Hello,

    J'aune classe dotée d'un membre constant de type unique_ptr, qui la plupart du temps ne sera pas associé à une donnée (pointera donc sur null). POur les cas où il doit l'être, l'initialisation se fait via un argument du constructeur, qui a une valeur par défaut (nullptr).

    Question 1 :
    Cet argument optionnel devrait-il être de type pointeur ou unique_ptr ? J'aurais tendance à opter pour le second (car l'utilisateur qui a assigné le pointeur ne fait pas face à une ambiguité sur le devenir de l'emplacement mémoire), mais cela induit à se poser des questions sur le coût de la construction (?) par rapport à un nullptr.

    Question 2 :
    La valeur du pointeur (ou de l'unique_ptr) est indiquée très loin en amont (l'argument est passé via 4 fonctions en cascade). Je pensais me mettre à la sémantique de mouvement, mais comment mettre ça en œuvre ? Les fonctions prendraient un unique_ptr<>&& ? Et l'argument est donné avec un std::move() ?

    Merci.

  2. #2
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut
    J'ai fait un truc du genre :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    struct MaClasse
    {
        MaClasse(std::unique_ptr<int>&& unEntier = nullptr) :
            m_Entier(std::move(unEntier ))
        { }
        std::unique_ptr<int>m_Entier;
    };
    Est-ce la bonne méthode ?

  3. #3
    Membre émérite

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Points : 2 252
    Points
    2 252
    Par défaut
    Je pense que le mieux est plutôt de passer l'unique_ptr par valeur, pour bien garantir que l'ownership a été transféré :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    struct MaClasse
    {
        MaClasse(std::unique_ptr<int> unEntier = nullptr) :
            m_Entier(std::move(unEntier ))
        { }
        std::unique_ptr<int>m_Entier;
    };
    Explication ici :

    http://stackoverflow.com/questions/8...-or-a-function

  4. #4
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut
    En fait, comme unique_ptr est non copiable, les passages par valeur et par r-value ont-ils une réelle différence ?

  5. #5
    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,

    Contrairement à l'explication donnée sur stackoverflow, je dirais qu'il faudrait mieux passer le pointeur nu (mais ne même pas faire appel à move).

    Mais avant d'aller plus loin, je dois préciser que je considère généralement que le unique_ptr n'est qu'un "détail d'implémentation" de la classe qui le manipule.

    Cette manière de considérer les choses oriente -- fatalement -- ma réflexion

    Je trouves en effet dommage d'exposer un tel détail d'implémentation s'il est possible de l'éviter, fusse au niveau du constructeur.

    Partant de là, je me dis que l'utilisateur n'a strictement aucun besoin de savoir sous quelle forme le pointeur sous-jacent est géré en interne par la classe et que c'est à la classe qui manipule le unique_ptr de savoir si elle transfère la propriété du pointeur sous-jacent ou si elle ne fait que le "prêter" dans le cadre d'une utilisation particulière, quitte à ce qu'elle expose deux comportements correspondant au deux possibilités:
    • un comportement "release" qui appelle la fonction équivalente au niveau du unique_ptr (qui a pour résultat de libérer la propriété du pointeur sans le détruire, pour permettre de transférer cette propriété) et
    • un comportement "get" (ou tout ce que tu veux d'autre) qui appelle la fonction équivalente au niveau de unique_ptr (qui a pour résultat de renvoyer le pointeur nu, tout en permettant au unique_ptr de garder la propriété du pointeur)
    (si tant est que cela ait du sens de permettre à l'utilisateur de récupérer le pointeur sous-jacent sous l'une ou l'autre forme, chacune d'elles n'étant ni automatique ni exclusive bien sur )

    Autrement dit, mon raisonnement tend à faire en sorte que l'utilisateur puisse passer un pointeur dont il sait qu'il n'est la propriété de rien au constructeur de la classe dont il sait que la classe prendra la propriété du pointeur en question sans avoir à s'inquiéter de "la popote interne" qui permettra à la classe en question d'acquérir la propriété de ce pointeur, et sans avoir à s'inquiéter de savoir si la classe en question des prête à "partager" le pointeur ou non .

    A l'utilisation, c'est donc à l'utilisateur de savoir, selon ce que la classe lui permet de faire, s'il veut transférer la propriété du pointeur sous-jacent ou non
    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

  6. #6
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut
    Avec un pointeur nu, on ne sait pas qui est supposé se charger de la libération de la ressource allouée par le pointeur. L'utilisateur va fatalement se poser la question : "est-ce à moi de désallouer ?"

  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
    Citation Envoyé par oodini Voir le message
    Avec un pointeur nu, on ne sait pas qui est supposé se charger de la libération de la ressource allouée par le pointeur. L'utilisateur va fatalement se poser la question : "est-ce à moi de désallouer ?"
    Oui et non.

    Cela dépend énormément du contexte, et des règles en application sur le projet à vrai dire

    Si tu dis une bonne fois pour toute que les objets pour lesquels l'allocation dynamique a été utilisée doivent, d'office, être pris en charge par une classe qui en prendra la responsabilité au travers d'une classe RAIIsante (comme cela, je parle aussi bien de unique_ptr que des deux autres ), la règle peut parfaitement ressembler à quelque chose comme
    on ne fait plus de delete sur un pointeur nu car c'est géré "par ailleurs".
    C'est peut être contre intuitif pour quelqu'un qui vient de C, je te l'accorde, mais ca facilite la compréhension d'un javaiste habitué au garbage collector (tout cela pour dire qu'il y aura des avantages et des inconvénients, comme pour toute règle "arbitraire" )

    A partir de là, ben, si tu décides d'utiliser unique_ptr, c'est pour assurer l'unicité référentielle, et donc, assurer une sémantique d'entité au pointeur sous-jacent.

    Cela implique (ou devrait impliquer) que la construction des différents objets est centralisée, ad minima au niveau d'une fabrique, idéalement dans un module spécifique.

    Dés lors, le seul moment où tu as réellement un pointeur nu, c'est à la sortie de ta fabrique, et il devrait donc être pris en charge par "quelque chose" qui en prendra la propriété et donc la responsabilité.

    Et une fois que la réponse à "qui prend la responsabilité de la gestion de cette ressource? " a sa réponse, tu n'as "plus qu'à" définir une politique de "passage de relais" éventuelle et / ou d'utilisation

    D'une certaine manière, je pense que c'était l'objectif visé par la proposition qui a mené à l'ajout des pointeurs intelligents, non (je n'ai jamais réellement lu l'abstract de la proposition en question, et je peux donc me tromper sur ce point )
    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
    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
    D'ailleurs, au pire, si tu crains que les gens ne se posent la question de savoir ce qu'ils doivent faire du pointeur qu'ils récupèrent, rien ne t'empêche d'utiliser la technique dite de l'objet nul :

    Tu définis une classe dérivée de ta classe de base pour laquelle tous les comportements indiquent clairement qu'il s'agit d'une erreur (attention, je ne parle pas de lancer une exception ici : ce sont soit des comportements vides, soit un return false, soit le renvoi d'une valeur invalide, ou des choses du genre )

    une fois que tu as cela, tu renvoie systématiquement tes objets par référence : vers un objet statique dont le type est cet objet nul si l'objet sous-jacent de ton unique_ptr est nul, vers l'objet pointé par le pointeur sous-jacent de ton unique_ptr s'il s'agit d'un objet valide

    L'exemple d'un int n'est peut etre pas le plus adapté, tu pourrais envisager 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
    class MaClass{
        public:
            MaClass(int * ptr = nullptr):item(ptr){}
            int /* const */ & get(){
                return item.get()? *(item.get()) : noValue;
                /* ou, si tu préfères */
                if( item.get()!= nullptr)
                    return *(item.get();
                return noValue;
            }
        private:
            std::unique_ptr item;
            static int noValue;
    };
    int MaClass::noValue = std::numeric_limits<int>::max();
    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

  9. #9
    Membre émérite

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Points : 2 252
    Points
    2 252
    Par défaut
    Citation Envoyé par oodini Voir le message
    En fait, comme unique_ptr est non copiable, les passages par valeur et par r-value ont-ils une réelle différence ?
    Oui, une petite différence : Le passage par valeur force le transfert de l'unique_ptr (car force la création d'un unique_ptr par constructeur de mouvement), alors que le passage par rvalue-reference non. Une r-value reference reste une référence avant tout, le corps de la fonction peut donc s'en servir comme une référence classique par exemple uniquement en la lisant.

    Par exemple avec le code suivant :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    void foo(std::unique_ptr<int> = nullptr){}
    auto iptr = std::make_unique<int>(42);
    //foo(iptr); // ne compile pas, copie interdite
    foo(std::move(iptr));
    // iptr vide ici
    Sans même regarder l'implémentation de foo() (qui ici est vide) l'appelant a la garantit que iptr est forcement mis à nullptr en sorti de foo(). La valeur sous-jacente a été transférée dans foo.
    Alors qu'avec le code suivant :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    void foo(std::unique_ptr<int>&& = nullptr){}
    auto iptr = std::make_unique<int>(42);
    //foo(iptr); // ne compile pas, copie interdite
    foo(std::move(iptr));
    // iptr inchangé !
    on a comportement complètement contre-intuitif, on vient d'écrire un "move" et pourtant la valeur d'iptr n'a pas du tout changé !

    C'est pour ça que je disais que le passage par valeur est un peu plus clair sur le transfert d'ownership, car l'appelant a la garantit qu'un unique_ptr passé à une fonction qui prend par valeur est vide après l'appel à la fonction et donc qu'il n'en a plus la responsabilité.

  10. #10
    Membre expert

    Avatar de germinolegrand
    Homme Profil pro
    Développeur de jeux vidéo
    Inscrit en
    Octobre 2010
    Messages
    738
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur de jeux vidéo
    Secteur : Tourisme - Loisirs

    Informations forums :
    Inscription : Octobre 2010
    Messages : 738
    Points : 3 892
    Points
    3 892
    Par défaut
    Pour ma part je prendrais l'une ou l'autre des solutions, mais surtout pas unique_ptr&&. J'ai essayé et c'est le meilleur moyen de faire n'importe quoi (en fait j'évite de prendre ce genre d'argument, je prends directement par valeur tant qu'à faire).

    Quand j'écris du code dont je fixe moi-même les conventions je prendrai un unique_ptr, cela indique clairement à l'utilisateur de la classe ce que je compte faire de ce qu'il me transmet sans aucune ambiguité possible, et le compilateur est le garant de ce bon fonctionnement.

    Contrairement à ce que dit Koala, un tel objet ne vient pas nécessairement d'une factory. En effet, cela peut être tout simplement que l'on fait des opérations avec l'objet avant de le transférer à la classe. Un exemple : vous avez un std::unique_ptr<Socket>, le centre de connexion va établir une connexion, et une fois que c'est fait il se débarrasse de la socket qui sera transférée ailleurs (dans le centre de communication par exemple). En cas de déconnexion, le centre de communication retransfère la socket au centre de connexion qui va tenter de se reconnecter et en cas de réussite transférer à nouveau au centre e communication, etc.

  11. #11
    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 germinolegrand Voir le message
    Contrairement à ce que dit Koala, un tel objet ne vient pas nécessairement d'une factory. En effet, cela peut être tout simplement que l'on fait des opérations avec l'objet avant de le transférer à la classe. Un exemple : vous avez un std::unique_ptr<Socket>, le centre de connexion va établir une connexion, et une fois que c'est fait il se débarrasse de la socket qui sera transférée ailleurs (dans le centre de communication par exemple). En cas de déconnexion, le centre de communication retransfère la socket au centre de connexion qui va tenter de se reconnecter et en cas de réussite transférer à nouveau au centre e communication, etc.
    Je me suis peut être très mal exprimé à ce sujet.

    Le fait que l'objet vienne d'office d'une fabrique est effectivement réducteur.

    Mais, dans l'exemple que tu cites, on en revient malgré tout au même : le socket est "créé" quelque part (au centre de connexion ) puis sa propriété est transférée à gauche ou à droite, et il revient à la classe propriétaire de savoir ce qu'elle doit en faire (le détruire, transférer la prorpiété ou le "prêter" à autre chose), quitte, si le centre de connexion se retrouve avec trop de socket "inutilisés", à ce qu'il décide, parce qu'il en est (redevenu) propriétaire, d'en détruire quelques uns parce que "cela ne sert à rien de garder 150 connexion actives quand seulement trois sont utilisées".

    Le problème, lorsqu'on commence à transmettre des unique_ptr, c'est que l'on a beaucoup trop vite fait "d'oublier" qu'une des fonctions que l'on appelle a appelé une fonction qui en a appelé une autre qui a transféré la propriété du pointeur à ... quelque chose qui a été détruit depuis (ou non) et donc que l'on risque de se retrouver, bien malgré soi, avec un effet de bord qui aura pour conséquence que le pointeur que l'on avait avant l'appel d'une fonction sera devenu invalide juste après cet appel.

    En transmettant le pointeur nu et en permettant (ou non) le transfert de responsabilité de manière explicite (tu transfères le pointeur issu de la fonction release() ), tu gardes "la main" sur ce qui est réellement fait, et tu le remarque beaucoup plus facilement.

    Comme je l'ai dit dans ma première intervention, le fait que la propriété d'un objet créé sur le tas soit géré par un unique_ptr à l'intérieur d'une classe n'est, à mon sens, qu'un "détail d'implémentation", tout comme on peut qualifier de détail d'implémentation le fait que le nom et le prénom d'une personne peuvent être représenter de différentes manière par une classe qui présenterait des fonctions name() et firstName():

    Ce peut être une structure composée de ces deux chaines de caractères, ce peut être deux chaines de caractères distinctes au niveau de la classe ou les chaines de caractères se trouvant aux indice 3 et 18 d'un tableau de chaines de caractères, (ou toute autre solution encore plus surprenante) on s'en coutre fout: ce qui importe, ces que ces chaines existent et que les fonctions name() et firstName() puissent remplir leur office.

    Ici, c'est pareil : on fournit (ou non) certains services qui ont justifié l'utilisation d'un unique_ptr, le reste (comprends : le fait que c'est effectivement géré en interne à partir d'un unique_ptr), cela ne regarde personne (en tout cas pas l'utilisateur de la classe): cela ne regarde que la classe en elle-même

    En fait, pour être clair, je plaide (de manière générale à chaque intervention sur ce forum) pour le fait que l'on ne se considère comme développeur d'une fonctionnalité que tant que le curseur de notre EDI préféré se trouve dans la portée de cette fonctionnalité et comme utilisateur de cette fonctionnalité dés que le curseur a quitté cette portée.

    C'est, très certainement, une "gymnastique intellectuelle" assez difficile à faire, mais l'idée est toujours la même:

    Poses toi, en tant que développeur de ta classe, et en respectant "à la lettre" certains principes de base (ici, c'est la loi de Déméter) la question de ce que tu veux permettre à l'utilisateur de ta classe, à ce qui prend sens à lui permettre (en fonction des services que l'utilisateur est en droit d'attendre de sa part).

    Une fois que tu as fini de développer ta classe, ta fonction, ta fonctionnalité, tu n'est plus que l'utilisateur de celle-ci, et tu n'as plus à t'intéresser qu'aux services qu'elle t'expose.

    Je crois, sincèrement, que l'on devient "meilleur développeur" une fois que l'on arrive à faire cette gymnastique
    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

  12. #12
    Membre expert

    Avatar de germinolegrand
    Homme Profil pro
    Développeur de jeux vidéo
    Inscrit en
    Octobre 2010
    Messages
    738
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur de jeux vidéo
    Secteur : Tourisme - Loisirs

    Informations forums :
    Inscription : Octobre 2010
    Messages : 738
    Points : 3 892
    Points
    3 892
    Par défaut
    À ce compte là, les std::string sont également un détail d'implémentation… et on ne devrait passer que des const char* ? (à noter au passage que des string::range ne seraient pas de trop, m'enfin je peux courir avant de voir ça dans le standard)

    Pourquoi les primitives dépourvues de toute sémantique devraient-elles être la solution ? Qui a dit que celui qui prend en argument un unique_ptr devait le stocker sous forme d'un unique_ptr ? Le raisonnement ne tient pas debout. Le unique_ptr en argument n'est en aucun cas un détail d'implémentation, mais bien un détail d'interface. La seule chose dont on est assuré c'est que la fonction qui prend un unique_ptr en argument prend la responsabilité complète de ce qu'on lui passe, libre à elle d'aller la stocker comme il veut, de faire un release() si ça lui chante et de gérer ça à sa sauce.

    La seule chose dont on est assuré avec une primitive, c'est... rien .
    Conclusion utiliser un pointeur nu ne me semble absolument pas justifiable de cette façon. (la seule justification assez feignasse d'ailleurs que j'ai reçue c'est que "c'est c*iant de créer un unique_ptr côté utilisateur, je préfère faire un new tout simple", problème résolu avec make_unique)

    Edit: visiblement le forum supporte pas les espaces insécables

  13. #13
    Expert éminent

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Points : 6 911
    Points
    6 911
    Par défaut
    Koala, en plus de documenter que la responsabilité de la libération du pointeur passe à l'appelant, l'utilisation d'unique_ptr<> (passé par valeur, nous sommes d'accord) dans l'interface est le moyen le plus simple

    - de s'assurer qu'elle sera remplie, même en présence d'exceptions (en particulier pendant l'évaluation des autres arguments, c'est une responsabilité impossible à assumer pour l'appelé et particulièrement pénible pour l'appelant sans unique_ptr<> et make_unique)

    - de fournir une information sur la manière dont cette libération doit être faite.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  14. #14
    Expert éminent

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Points : 6 911
    Points
    6 911
    Par défaut
    Citation Envoyé par germinolegrand Voir le message
    (à noter au passage que des string::range ne seraient pas de trop, m'enfin je peux courir avant de voir ça dans le standard)
    Tu penses à http://isocpp.org/files/papers/N3762.html ou à autre chose?
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  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
    Citation Envoyé par germinolegrand Voir le message
    À ce compte là, les std::string sont également un détail d'implémentation… et on ne devrait passer que des const char* ? (à noter au passage que des string::range ne seraient pas de trop, m'enfin je peux courir avant de voir ça dans le standard)
    Non, ce que je dis, c'est qu'on profite d'un certain nombre de services grâce à la classe string, et que c'est au travers de ces services qu'on l'utilise.

    Ce que je dis, c'est qu'il n'y a strictement aucune raison cohérente pour qu'il en aille différemment en ce qui concerne les classes que nous développons nous même.

    Ce que je dis, c'est que le paradigme orienté objets nous donne "tous les outils" pour pouvoir réfléchir en terme de services rendus et non en fonction des données qui permettent de rendre ces services et que lorsqu'on développe une classe (quelle qu'elle soit), on devrait la développer en fonctions des services qu'on en attend.

    Ce que je dis, c'est qu'une fois qu'on a fini de développer les services que l'on attend de la part d'une classe, on n'a strictement aucun besoin de savoir comment ce service est rendu, pas plus que l'on a besoin de s'inquiéter de la manière dont la classe std::string fait pour rendre les services qu'on utilise.

    Pourquoi les primitives dépourvues de toute sémantique devraient-elles être la solution ? Qui a dit que celui qui prend en argument un unique_ptr devait le stocker sous forme d'un unique_ptr ? Le raisonnement ne tient pas debout. Le unique_ptr en argument n'est en aucun cas un détail d'implémentation, mais bien un détail d'interface. La seule chose dont on est assuré c'est que la fonction qui prend un unique_ptr en argument prend la responsabilité complète de ce qu'on lui passe, libre à elle d'aller la stocker comme il veut, de faire un release() si ça lui chante et de gérer ça à sa sauce.
    Je n'ai jamais dit une chose pareille

    Ce que j'ai dit, c'est que chaque classe doit prendre ses propres responsabilités, et qu'à partir du moment où elle prend la responsabilité de la durée de vie d'un pointeur nu, l'utilisateur n'a aucun besoin de savoir de quelle manière cette responsabilité est prise en charge : il doit se baser sur les services rendus par cette classe afin de savoir ce qu'il peut faire (ou non) du pointeur sous-jacent.

    Ce que je dis, c'est que lorsqu'une classe prend la responsabilité de la durée de vie d'un objet au travers d'un unique_ptr, la responsabilité de l'utilisateur est de lui fournir un pointeur qui n'est plus la propriété, d'aucun autre objet, à charge pour lui de s'assurer que les services de la classe qui était responsable de l'objet lui permettent de renoncer à la propriété en question.

    La seule chose dont on est assuré avec une primitive, c'est... rien .
    Conclusion utiliser un pointeur nu ne me semble absolument pas justifiable de cette façon. (la seule justification assez feignasse d'ailleurs que j'ai reçue c'est que "c'est c*iant de créer un unique_ptr côté utilisateur, je préfère faire un new tout simple", problème résolu avec make_unique)
    Je n'ai au grand jamais plaidé contre l'utilisation des pointeurs intelligents, je plaide juste pour une utilisation intelligente des concepts qu'ils représentent.

    Si tu as une classe A et une classe B qui ont toutes les deux dans leurs attributions de gérer la durée de vie d'un objet de type X créé sur le tas, pas de problème.

    Mais ce qui intéresse l'utilisateur des classes A et B est de savoir que ces deux classes agissent de la même manière en ce qui concerne l'objet de type X, et non de savoir comment elles s'y prennent.

    Il appartient donc au développeur de la classe A de permettre prendre cette responsabilité et de lui permettre (ou non) de s'en libérer sans avoir à s'inquiéter de l'origine du pointeur sur X, tout comme il appartient au développeur de la classe B de permettre à sa classe de prendre cette responsabilité et de lui permettre (ou non) de s'en libérer sans avoir à s'inquiéter de savoir si le pointeur vient de la classe A ou s'il vient de "n'importe où ailleurs" (et pas forcément sous la forme d'un unique_ptr).

    Enfin, c'est à l'utilisateur des classes A et B de savoir que ces classes prennent la responsabilité de la durée de vie du pointeur vers X qu'il leur transmet, et donc de veiller à ce que la durée de vie du pointeur qu'il leur transmet (quelle que soit son origine) ne soit pas déjà prise en charge par "autre chose".

    S'il utilise les classes A et B en même temps, comme il n'a strictement aucun besoin de savoir que A et B gèrent la durée de vie du pointeur sur X au travers d'un unique_ptr, ce qu'il doit être en mesure de faire, c'est dire à l'instance de A (ou à l'instance de B) : file moi le pointeur (sur X), car je te libère de la responsabilité de sa durée de vie (sous entendu : pour la filer à quelqu'un d'autre )

    Si l'utilisateur veut maintenir le pointeur qu'il récupère de la sorte depuis l'instance de A (ou depuis l'instance de B) sous la forme d'un unique_ptr, le temps d'être en mesure de créer une instance d'une autre classe qui prendra la responsabilité de la durée de vie de son pointeur, libre à lui.

    Mais encore une fois, la classe qui prendra la responsabilité de la durée de vie du pointeur n'a que faire de savoir la manière dont cette responsabilité était prise en charge avant qu'elle ne la prenne: c'est à l'utilisateur de fournir un pointeur pour lequel aucune classe ne va essayer de le détruire par inadvertance
    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
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    @Arzar: Pour revenir à ce que tu disais sur le choix rref/value. Ton argument n'est pas le bon selon moi.

    En effet une fonction peut se servir d'une rref juste pour lire (*), cependant si l'utilisateur à écris un move c'est qu'il dit explicitement que lui il en veut plus, il devrait se moquer de savoir ce que vaut l'objet ensuite (**).

    Dans le cas de l'OP, l'argument est plus simple, si tu utilises une rref tu ne peux plus initialiser depuis un pointeur nu (***), cependant on pourrait envisager deux versions (et dans ce cas une T* et une unique_ptr<T>&& est ce qui convient, AMA).

    (*) Même si c'est une erreur selon moi, dans ce cas un const T& convient bien mieux, AMA.

    (**) Du moins tant qu'il effectue pas une réaffectation ou un service de RaZ.

    (***) A condition qu'on envisage ce cas bien entendu, ce dont je ne suis pas convaincu.

  17. #17
    Membre expert

    Avatar de germinolegrand
    Homme Profil pro
    Développeur de jeux vidéo
    Inscrit en
    Octobre 2010
    Messages
    738
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur de jeux vidéo
    Secteur : Tourisme - Loisirs

    Informations forums :
    Inscription : Octobre 2010
    Messages : 738
    Points : 3 892
    Points
    3 892
    Par défaut
    J'ai toujours pas compris l'intérêt du pointeur nu dans l'histoire.

    Si je veux faire un système interne à base de shared_ptr mais que je veux être seul à gérer la vie de l'objet (ou avec d'autres classes qui prennent des shared_ptr, mais osef du point de vue utilisateur, c'est un détail interne ce que je fais de l'objet dont j'ai la responsabilité), je prendrai un unique_ptr en argument. Ca indique : tu me donnes un objet dont je prends l'entière responsabilité, charge à toi d'assurer ta part du contrat qui est de fournir exactement ce que j'ai sémantiquement demandé (oui si l'utilisateur a envie de faire n'importe quoi explicitement avec son utilisation de la classe, c'est son problème, comme toujours en C++, i fight against Murphy not Machiavel).

    Si je veux permettre à l'utilisateur de conserver un morceau de responsabilité sur sa classe (il est pas obligé hein, il peut me la filer complètement), je prendrai un shared_ptr. Si tout ce qui m'intéresse c'est un accès à l'objet, pas sa responsabilité, je prendrai tout bêtement une référence en argument, qu'importe si je la stocke en pointeur nu, c'est un détail. C'est justement utiliser correctement les pointeurs intelligents ou les références pour construire une interface qui se suffit à elle-même, quel intérêt de remplacer une vrai sémantique par un tas de commentaires (dont on sait tous mauvaise idée que c'est :-° cf. le sujet à troll, on va pas rouvrir ça ici) qui vont dire au programmeur qui les lira (ou pas) ce que le compilateur ne saura jamais...

  18. #18
    Membre expert

    Avatar de germinolegrand
    Homme Profil pro
    Développeur de jeux vidéo
    Inscrit en
    Octobre 2010
    Messages
    738
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur de jeux vidéo
    Secteur : Tourisme - Loisirs

    Informations forums :
    Inscription : Octobre 2010
    Messages : 738
    Points : 3 892
    Points
    3 892
    Par défaut
    pour les string::range, oui c'est probablement un truc à la sauce de string_view, j'en ai implémenté sur mes Path afin de ne pas être forcé de copier une chaîne de caractère quand j'ai juste besoin d'analyser un morceau du path.

  19. #19
    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 germinolegrand Voir le message
    J'ai toujours pas compris l'intérêt du pointeur nu dans l'histoire.

    Si je veux faire un système interne à base de shared_ptr mais que je veux être seul à gérer la vie de l'objet (ou avec d'autres classes qui prennent des shared_ptr, mais osef du point de vue utilisateur, c'est un détail interne ce que je fais de l'objet dont j'ai la responsabilité), je prendrai un unique_ptr en argument. Ca indique : tu me donnes un objet dont je prends l'entière responsabilité, charge à toi d'assurer ta part du contrat qui est de fournir exactement ce que j'ai sémantiquement demandé (oui si l'utilisateur a envie de faire n'importe quoi explicitement avec son utilisation de la classe, c'est son problème, comme toujours en C++, i fight against Murphy not Machiavel).
    Le fait est que, selon moi, en tant que développeur de la classe, tu n'as pas à te soucier d'où vient le pointeur!

    Tu veux que ta classe prenne la responsabilité de la durée de vie l'objet très bien, elle prend la responsabilité de la durée de vie de l'objet.

    Tu veux pouvoir libérer ta classe de la responsabilité de la durée de vie de l'objet très bien, tu exposes (faisant appel à la fonction release de unique_ptr) un service qui le fait explicitement.

    Tu veux que ta classe soit en mesure de "prêter" l'objet à "autre chose" tout en gardant la responsabilité de la durée de vie de l'objet Très bien, tu exposes un service adéquat (faisant appel à la fonction get de unique_ptr)

    Tu veux que l'utilisateur soit en mesure de détruire explicitement l'objet géré par ta classe très bien, tu expose un service (faisant appel à la fonction reset de unique_ptr) qui en donne l'occasion.

    Tu n'as pas à t'inquiéter de l'origine de l'objet, ni de la manière dont la durée de vie était prise en charge avant que cette responsabilité ne soit transférée à ta classe, tu as juste à partir du principe que "personne d'autre" ne prend la durée de vie de l'objet en charge.
    Si je veux permettre à l'utilisateur de conserver un morceau de responsabilité sur sa classe (il est pas obligé hein, il peut me la filer complètement), je prendrai un shared_ptr. Si tout ce qui m'intéresse c'est un accès à l'objet, pas sa responsabilité, je prendrai tout bêtement une référence en argument, qu'importe si je la stocke en pointeur nu, c'est un détail.
    tout comme le fait que tu le stockes sous la forme d'un pointeur intelligent n'est qu'un détail que l'utilisateur n'a pas besoin de connaitre
    C'est justement utiliser correctement les pointeurs intelligents ou les références pour construire une interface qui se suffit à elle-même, quel intérêt de remplacer une vrai sémantique par un tas de commentaires
    Justement, il s'agit de créer une interface qui soit cohérente par rapport aux services que tu es en droit d'attendre de la part de la classe que tu développes, et que tu dois essayer de développer en y mettant le moins de contraintes possible.
    (dont on sait tous mauvaise idée que c'est :-° cf. le sujet à troll, on va pas rouvrir ça ici) qui vont dire au programmeur qui les lira (ou pas) ce que le compilateur ne saura jamais...
    S'il y a bien une sorte de commentaire qui ne pose aucun problème, c'est bel et bien les commentaires qui servent de cartouche, ceux qui indiquent comment utiliser tes classes et les fonctions qu'elles exposent

    Ton boulot, en tant que développeur d'une classe A, n'est pas de veiller dans la classe A à ce que la classe Y ou Z fasse bien son boulot (enfin, on se comprend hein ) ton boulot consiste à veiller à ce que la classe A fasse correctement son boulot, et à expliquer à l'utilisateur la manière dont il devra l'utiliser, ce qui implique (comme dans le cas présent) parfois des prérequis comme "rien ne doit prendre la durée de vie du pointeur transmis en charge".

    Une fois que tu as développé ta classe et que tu as indiqué à l'utilisateur comment tu as prévu qu'elle fonctionne, tu deviens l'utilisateur de la classe et tu veilles à l'utiliser de la manière "prévue par le manuel" .

    Alors, bien sur, tu te rendras peut être compte que ta classe A ne fournit pas tous les services dont tu as besoin en tant qu'utilisateur, et tu pourras, le temps de les rajouter, reprendre la casquette de développeur de ta classe A... Pour l'abandonner à nouveau lorsque tu auras fini d'implémenter le service en question

    Mais, l'un dans l'autre, tes classes fournissent un certains nombre de services et la manière dont elles s'y prennent pour se faire n'intéresse absolument pas l'utilisateur, qui doit juste veiller à ce que les éventuels prérequis (indiqués par le développeur de la classe) soient respectés .
    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

  20. #20
    Expert éminent

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Points : 6 911
    Points
    6 911
    Par défaut
    Le fait est que transférer une responsabilité pose un certain nombre de problèmes. std::unique_ptr offre une solution standard à ces problèmes quand la responsabilité est la libération d'un pointeur. Oui, il y a des cas où ces problèmes ne se posent pas. Oui, il y a d'autres moyens de les résoudre que std::unique_ptr, mais ils sont plus fastidieux ou reviennent à utiliser std::unique_ptr. L'utilisation systématique de unique_ptr te permet d'éviter à devoir te poser la question et de revenir remettre ton ouvrage sur le métier une cent-et-unième fois quand un utilisateur de ta classe trouve insupportable de ne pas pouvoir l'utiliser avec un pointeur alloué dans un pool.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

Discussions similaires

  1. Problème avec la sémantique de "transient"
    Par professeur shadoko dans le forum Langage
    Réponses: 10
    Dernier message: 06/06/2014, 15h24
  2. Collisions avec un objet en mouvement
    Par xoux28 dans le forum Tkinter
    Réponses: 15
    Dernier message: 30/03/2014, 11h49
  3. Réponses: 1
    Dernier message: 14/05/2012, 18h54
  4. ouverture avec une interpolation de mouvement d'une fenetre
    Par escteban dans le forum Général JavaScript
    Réponses: 7
    Dernier message: 19/06/2007, 17h04

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