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

SL & STL C++ Discussion :

Intérêt de std::unique_ptr


Sujet :

SL & STL C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert

    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 : 35
    Localisation : France, Doubs (Franche Comté)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Par défaut
    Citation Envoyé par Freem Voir le message
    _ code plus chiant à écrire
    std::unique_ptr c'est "juste" l'implémentation de l'idée du RAII. Si tu t'en passes tu te retrouveras de toute façon à recréer ces capsules.
    Citation Envoyé par Freem Voir le message
    _ erreurs de compilations encore plus longues à lire (un template de plus quoi)
    J'ai pas testé depuis longtemps, mais il y a normalement des outils qui sont là pour rendre ces erreurs lisibles.
    Citation Envoyé par Freem Voir le message
    _ pas plus de protection que ça contre les delete stupides, puisqu'il faut utiliser get() qui file un pointeur brut si on veut permettre à quelqu'un d'autre (méthode, fonction, peu importe) de modifier le contenu.
    L'objectif de std::unique_ptr c'est d'offrir un élément permettant de clairement définir et identifier la responsabilité de la libération d'une ressource. Qu'est ce qu'un "delete stupide" ? Avec std::unique_ptr tu n'as même plus besoin de te préoccuper du delete (c'est le but du RAII).
    Citation Envoyé par Freem Voir le message
    _ 0 intégration dans d'autres lib
    Une partie des bibliothèques n'ont pas à se préoccuper de ça : elles n'ont pas de pointeur dans leur interface. Une partie de celle qui reste travaille avec des pointeurs nues pour signaler qu'elles prennent la responsabilité des données, l'absence de prises en compte des pointeurs intelligents n'est pas un problème (si la bibliothèque est destiné à avoir la responsabilité des données, alors le choix d'utiliser un pointeurs intelligent simultanément est critiquable). On est loin de 0 ...

    Pour la citation que j'ai en signature, le seul contexte où je l'ai vue c'est en introduction d'un chapitre du livre de David Abrahams sur boost::mpl, et je t'assure que c'était pas ironique.

  2. #2
    Membre éclairé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Décembre 2008
    Messages
    836
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Décembre 2008
    Messages : 836
    Par défaut
    Je n'avais même pas remarqué que tu l'avais en signature
    Je l'ai vue dans divers documents, certains le prenant au sérieux, d'autres, le prenant en ironie.
    D'autres aussi s'en amusent (d'aucuns diraient qu'ils se moquent): https://en.wikipedia.org/wiki/Fundam...re_engineering
    Mais la n'est pas le sujet.

    En retrouvant un peu mon sang-froid (j'admets l'avoir perdu face à la Nième galère que me file ce truc pour un gain nul) je me dis que, peut-être, il existe une situation ou c'est utile et simplifie effectivement le travail, mais que j'ai trop voulu faire confiance à ceux qui en prônent l'usage partout, alors que dans les cas ou j'ai voulu tester ce n'est pas le cas (par chance, projet perso, donc idéal pour essayer des trucs dont on est pas trop sûrs, mais le temps perdu est toujours frustrant)

    Pour moi, je n'en vois pas encore l'intérêt. La RAII (que j'adore), je l'utilise depuis longtemps et sans pointeurs intelligents. Au final, je m'en sors aussi bien.

    Pour ce qui est des erreurs de compilation, j'utilise actuellement gcc 4.7.1-2 (debian) et c'est toujours pénible. Même si effectivement ça s'est un peu amélioré.

    Pour le "delete stupide", je veux dire que simplement unique_ptr permet de donner le pointeur contenu (unique_ptr::get()), et donc l'expose à un delete, qui risque ainsi de rendre le pointeur intelligent instable.
    Naturellement, il faut faire confiance à l'intellect de l'autre... Sauf que le but du pointeur intelligent me semblait justement d'avoir à éviter de faire confiance a quelqu'un qu'on ne connaît pas nécessairement.
    D'ailleurs, j'ai constaté certains faits intéressants au sujet de unique_ptr: dans certaines situations, j'ai été obligé de faire une boucle parcourant un vector<unique_ptr<truc>> pour faire un reset() sur chaque objet. Sans ça, je me retrouvais avec des crash à cause d'une libération de mémoire qui semblait être effectuée deux fois (de mémoire, ça remonte à pas mal de temps). Pour coup, ça fait chier de faire un destructeur avec une vulgaire boucle de delete.... Pardon, de reset() (qui, sans argument, reviens à un delete).

    Pour ce qui est librairies...
    Il se trouve qu'elles ne me semblent pas toutes si propres que ça, ou que la doc est parfois de qualité moyenne sur le point de la propriété.
    Parfois, on passe un pointeur la ou une référence suffirait, et il est donc encore à nôtre charge de nettoyer.
    Il reste toujours des restes du langage C dans les habitudes de certains, quand les références n'existaient pas. Et où ces pointeurs (ou du moins certains d'entre eux) sont utilisés de la même façon que dans scanf.

    D'ailleurs, unique_ptr est justement là pour expliciter le transfert de responsabilité, à priori. Quand on passe un unique_ptr, on est obligé d'utiliser move, et il faudrait donc être simplet pour se dire qu'on a encore la responsabilité. Donc justement, quand on prend la responsabilité d'une zone mémoire, le unique_ptr me semblait pertinent.

    Naturellement, je n'ai pas une immense expérience dans le dev, alors je me dis que je peux me planter.
    J'aimerai bien voir un projet (de petite taille ou taille moyenne, pas un truc de 3M de lignes) qui s'en sert de façon pertinente, ça me permettrait sûrement de voir là ou ça apporte quelque chose.

  3. #3
    Membre Expert

    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 : 35
    Localisation : France, Doubs (Franche Comté)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Par défaut
    Pour les outils, je pensais plutôt à des choses comme STLfilt, ou à clang.

    Un exemple simple :
    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
     
    struct RessourceType1;
    struct RessourceType2;
     
    class MyClass
    {
      std::unique_ptr<RessourceType1> r1;
      std::unique_ptr<RessourceType1> r2;
     
    public:
      MyClass(/*params*/)
      {
        //Construit une ressource ou les deux selon les paramètres
      }
    };
    Le but de std::unique_ptr est bien de dire : "voici une ressource, tu en es responsable", l'utilisation de move permet effectivement de "déplacer" la responsabilité, mais si tu mets move c'est que tu veux justement la déplacer.

    Pour ton problème avec std::vector, tu as testé quand ? J'ai pas suivi l'ensemble de l'intégration de la move-semantic et des pointeurs intelligents dans gcc, mais il y a pu avoir des problèmes au début.

    Pour ce qui est de get, la philosophie de C++ c'est de laisser l'utilisateur libre, en particulier libre de se tirer dessus, si tu utilises un std::unique_ptr et que tu fais un delete p.get(); alors c'est que tu cherches les ennuies. Le choix de cette fonction get est surement du à ce que tu exprimes ensuite : des bibliothèques qui utilisent des pointeurs là où des références iraient (pour une raison X ou Y, c'est pas la question).

    Je suis d'accord avec toi sur le fait que toutes les bibliothèques ne proposent pas encore l'utilisation de pointeurs intelligents dans leur interfaces même quand ceux-ci seraient appropriés, cependant il faut minimiser ce besoin : ce sont des capsules, elles offrent de quoi les décapsuler au besoin.

  4. #4
    Membre éclairé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Décembre 2008
    Messages
    836
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Décembre 2008
    Messages : 836
    Par défaut
    clang, je n'ai pas encore réellement testé (2-3 tentatives mais je ne sais plus pourquoi je n'était pas parvenu à m'en servir et ai lâché l'affaire), c'est d'ailleurs sur ma TODO liste, vu qu'il paraît que la compilation est sensiblement plus rapide avec. STLfilt, je n'en ai même jamais entendu parler. Un item de plus pour la liste

    Pour le problème avec vector, je crois que ça remonte à quelques mois, 6 tout au plus. Il faudrait que je ré essaye, ou que je lise la norme pour connaître le comportement exact que doit avoir unique_ptr (si ça se trouve, le problème, c'est moi, vu qu'un pointeur il faut le nettoyer quand on s'en sers plus, et qu'ils ont voulu mimer au maximum le comportent d'un vrai)

    Pour le get, je sais bien que le C++ nous laisse libres. C'est d'ailleurs un peu pour ce petit plaisir maso qu'il reste mon favori. Seulement... je trouve ça tout de même paradoxal qu'un pointeur intelligent puisse permettre d'avoir le pointeur interne. Un peu comme si on pouvait manipuler la mémoire interne d'un vector en somme.
    D'un autre côté, vu qu'un certain nombre de lib n'existent pas en C++, je peux le comprendre.

    Pour le reste, minimiser le besoin... Vu que l'outil est récent, je comprend aussi qu'il ne soit pas super utilisé. Mais je pense que ce n'est pas que l'âge: de l'utilisation que j'en ai eue, il n'est pas pratique à manier.
    Il est plus simple au final (à mon goût) de faire un jeu de classes de bas niveau qui encapsulent les pointeurs nécessaires et les fonctions C-style qui peuvent les manier que d'utiliser std::unique_ptr. Et pas qu'un peu.
    L'intérêt de cette technique comparé au unique_ptr, c'est qu'on se retrouve avec la possibilité d'encapsuler les méthodes maniant le pointeur, plutôt que de continuer à faire appel à elles explicitement. Et si besoin est, il reste possible de surcharger les operateurs de cast. Ce type de classe, on les écrit une fois seulement pour chaque lib, et en utilisant l'inlining, les performances sont presque les même que du code C (reste le this à passer quand même, mais le compilo l'optimise peut-être.)
    D'autant que ça permet de dégager les dépendances à une librairie en dehors du chemin de code contenant réellement la logique du programme (et quand je vois le code de flare ou l'auteur aurait pu appeler ses pointeurs yatouquipet, je trouve vraiment cette solution élégante)

    Le souci, c'est en fin de compte le polymorphisme combiné aux conteneurs... (et sûrement un mauvais reste du C dans mes manies, vu que j'ai du mal à penser à dé-référencer un pointeur pour en faire une référence - je viens de m'en apercevoir en écrivant ce post - ).
    La solution la plus agréable aurait sûrement été de pouvoir utiliser directement des références dans les conteneurs, plutôt que de s'ennuyer à manier du pointeur (et risquer de stocker un pointeur vide. D'ailleurs, je me demande quelle est la raison de cette limitation? En interne, j'ai toujours vu les ref comme des ptr...).
    Au lieu de ça, on doit se taper des lignes dignes de JAVA, les avantages en moins (std::vector<std::unique_ptr<montemplate<maclasse>>> n'est quand même pas super causant au 1er coup d'oeil, et ce n'est qu'une déclaration.).
    Vu que personnellement, je réfléchis à 2 fois avant de mettre une donnée en protected (et 3 fois pour le public), je n'accède que rarement au contenu d'un conteneur polymorphique en dehors de l'objet qui le possède.
    Hors, quand je manie une donnée via un pointeur, je fait gaffe, je sais que ça peut péter.
    Encore une fois le remède unique_ptr me semble pire que le mal brut.

    Et pour des débutants, je ne sais pas non plus si je le recommanderais. Dire que c'est de la sécurité, c'est aussi vrai que de dire que les problèmes de mémoire sont inexistants dans les langages à GC. Sans vouloir être mesquin, c'est une fausse sécurité puisqu'elle donne un sentiment erroné, en plus de ralentir et complexifier la compilation (et les templates ne sont pas des modèles de rapidité)

    Reste plus qu'a avoir la manie d'uniformiser le code, et je me suis retrouvé à les mettre a chaque fois que je devais manier un pointeur (sont faits pour ça il paraît). Et sincèrement, je le regrette amèrement.

    En guise de conclusion de ce long post, je dirais ceci:
    Contrairement à la plupart des avis que j'ai vus, je ne le recommande pas aux débutants, parce que leur usage correct implique une vraie compréhension de la mécanique qu'ils utilisent ainsi que de std::move (parce que C++ étant axé performance, je parie que la plupart des librairies ne mettent pas le conteneur de l'objet déplacé à nullptr, ce qui peut amener des surprises).
    En plus de ça, ça donne un faux sentiment de sécurité, pas mal de prises de têtes lors de la compilation, un code moins lisible si c'est mal employé (comme je l'ai dis, je pense avoir compris une de mes erreurs en écrivant ce post, à vérifier cependant).

    En fait, mal utilisé, les avantages que j'ai souvent vus cités (à savoir, meilleure lisibilité et robustesse du code) sont vite inversés:
    _ code plus lourd, lignes plus longues (et pas qu'un peu: std::unique_ptr<> ça mesure 17 caractères! Je sais qu'on a des écrans larges mais quand même)
    _ instabilité accrue (les compilateurs visent la performance, pas la sécurité en général. Donc, le conteneur originel n'est pas remis à zéro. Son contenu reste donc valide un temps, pour péter ensuite, loin de la dernière modif)
    _ moins bonne encapsulation (on se dit qu'il n'y a plus besoin d'encapsuler ce damné pointeur puisqu'unique_ptr le fait déjà. Résultat, on peut se retrouver avec du code spécifique à une librairie dans une zone qui pourrait très bien en être dépourvue si une classe avait encapsulé les appels de fonction et la gestion du pointeur. Du coup, on perd un avantage de l'objet: la facilité de virer une lib pour en mettre une autre)

    Éventuellement... peut-être qu'un débutant C++ qui n'a jamais fait de C s'en servirait correctement.

    Et vu que je ne maîtrise pas encore le C++ je pense qu'il vaut mieux pour moi les oublier quelques temps. Le temps de penser à réfléchir à 2 fois quand je tape "*" devant une variable, en fait.
    Bref, merci de les avoir défendus, ça m'a permis de réfléchir un peu plus loin. N'empêche... Je ne m'en servirai pas de sitôt pour autant.

  5. #5
    Membre Expert

    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 : 35
    Localisation : France, Doubs (Franche Comté)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Par défaut
    Tu as complètement occulté l'exemple que j'ai écrit où l'utilisation de std::unique_ptr (ou boost::scoped_ptr ou équivalent) est quasi-indispensable.

    J'ai l'impression que la plupart de tes réticences sont liées à la move-semantic, mais l'utilisation de la move-semantic est loin d'être indispensable à beaucoup de code (du moins pour l'utilisateur, ce que fait l'implémentation des éléments utilisés c'est autre chose), et ne pas l'utiliser ce n'est pas s'interdire std::unique_ptr.

    Le besoin de la move-semantic avec std::unique_ptr c'est juste quand tu as besoin de déplacer la responsabilité de la durée de vie, ton design n'est pas obligé de nécessiter de tel déplacement.

    Pour ce qui est des conteneurs, je suis d'accord que ca fait une écriture à rallonge, sauf que :


    Un pointeur intelligent c'est, d'une certaine façon, un conteneur de pointeur à un seul élément (ie une capsule), tu trouves étrange de pouvoir accéder à cet élément ? Pourtant accéder aux éléments d'un conteneur ne doit pas te choquer ?

    Il est plus simple au final (à mon goût) de faire un jeu de classes de bas niveau qui encapsulent les pointeurs nécessaires et les fonctions C-style qui peuvent les manier que d'utiliser std::unique_ptr.
    C'est bien moins flexible (et c'est redondant), l'utilisation des pointeurs intelligents (plus exactement : de capsule RAII non invasive), permet d'avoir un contrôle plus fin sur les points de variation du design. Avec ta technique si tu veux changer la gestion de la ressource tu te retrouves à modifier ta classe représentant la ressource, avec une capsule style std::unique_ptr (et une bonne utilisation des typename), tu as juste à changer une ligne totalement extérieur à la classe représentant ta ressource.

    La solution la plus agréable aurait sûrement été de pouvoir utiliser directement des références dans les conteneurs, plutôt que de s'ennuyer à manier du pointeur (et risquer de stocker un pointeur vide. D'ailleurs, je me demande quelle est la raison de cette limitation? En interne, j'ai toujours vu les ref comme des ptr...)
    Regardes les std::reference_wrapper, tu pourras les mettre dans les conteneurs. Pour pointeur/référence, il est en effet probable qu'en interne l'implémentation des références et des pointeurs soit similaires car les processeurs ne doivent pas offrir de quoi faire une distinction. Par contre d'un point de vue C++ (ie la façon dont le langage les définit) la différence existe : un pointeur est un objet (au sens du modèle de la mémoire en C++) alors qu'une référence n'est qu'une variable et pas un objet. D'un point de vue sémantique, un pointeur a une sémantique de référence (dans le sens où elle peut servir à référer) mais pas seulement (ils ont une notion d'arithmétique), alors qu'une référence C++ a seulement la sémantique de référence.

    Edit: Je ne pense pas non plus que tout code doit nécessairement contenir des std::unique_ptr/boost::scoped_ptr, ca dépend du code. Par contre s'en passer là où il est approprié est, AMA, une erreur. Pour la sérialisation, j'y connais pas grand chose, cependant ce n'est pas la partie d'un design où le besoin de pointeurs intelligents me parait évident : l'important pour cet élément ce sont les données, pas qui en a la responsabilité, sérialiser les capsule-RAII est-il nécessaire ?

  6. #6
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Yvelines (Île de France)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Par défaut
    Juste une réponse en passant sur un point. Autant pour un shared_ptr, accéder au pointeur contenu ne doit avoir lieu que pour de l'interfaçage avec du vieux code, autant pour un unique_ptr, je pense que ça peut intervenir plus souvent.

    En effet, côté shared, on a un couple shared_ptr/weak_ptr. Côté unique, le même couple s'écrit aujourd'hui unique_ptr/T* (et je n'ai pas réussi à vraiment convaincre les gens qu'une telle écriture n'est pas idéale).

    Il y a donc, en collaboration avec unique_ptr la possibilité tout à fait légitime et justifiée d'avoir des T* qui représentent des pointeurs n'influençant pas la durée de vie de l'objet pointé, et agissant en simples observateurs de l'unique_ptr (il faut bien entendu que l'architecture du programme permette de s'assurer que ce dernier restera vivant tant que l'observateur peut être utilisé). Et le moyen d'obtenir un T* à partir d'un unique_ptr, c'est justement get.

    Je pense que la règle à la louche n'est donc pas de faire attention aux get, ni aux pointeurs nus. Mais de faire attention aux delete. Un delete dans du code moderne, sauf s'il est dans l'implémentation d'un wrapper type smart pointer, et probablement un code smell. Pourquoi doit-on le faire manuellement, et ne risque-t-on pas de marcher sur les plates-bandes d'un autre mécanisme de gestion de la mémoire.
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  7. #7
    Membre éclairé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Décembre 2008
    Messages
    836
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Décembre 2008
    Messages : 836
    Par défaut
    Citation Envoyé par Flob90 Voir le message
    Tu as complètement occulté l'exemple que j'ai écrit où l'utilisation de std::unique_ptr (ou boost::scoped_ptr ou équivalent) est quasi-indispensable.
    Désolé.
    Le souci de ton exemple simple, c'est qu'il l'est trop.
    Je veux dire, il est tellement simple qu'on ne voit pas véritablement l'intérêt d'utiliser unique_ptr, excepté le fait de ne pas avoir à implémenter de destructeur.
    Du coup, je n'ai pas grand chose à dire à son sujet

    Citation Envoyé par Flob90 Voir le message
    J'ai l'impression que la plupart de tes réticences sont liées à la move-semantic, mais l'utilisation de la move-semantic est loin d'être indispensable à beaucoup de code (du moins pour l'utilisateur, ce que fait l'implémentation des éléments utilisés c'est autre chose), et ne pas l'utiliser ce n'est pas s'interdire std::unique_ptr.

    Le besoin de la move-semantic avec std::unique_ptr c'est juste quand tu as besoin de déplacer la responsabilité de la durée de vie, ton design n'est pas obligé de nécessiter de tel déplacement.
    En fait, c'est l'intérêt général de unique_ptr que je ne vois plus. Pendant un temps, je lui en trouvais un, mais ce n'est plus le cas.
    Effectivement, pour moi, si on ne déplace pas un objet d'un propriétaire a l'autre, alors les unique_ptr n'ont pas vraiment d'intérêt.
    Dans ce cas, un pointeur nu, un new dans le constructeur et un delete dans le destructeur et c'est bon.
    Utiliser un unique_ptr pour ça c'est un peu prendre un bazooka pour partir à la chasse aux mouches.

    Citation Envoyé par Flob90 Voir le message
    Pour ce qui est des conteneurs, je suis d'accord que ca fait une écriture à rallonge, sauf que :


    Un pointeur intelligent c'est, d'une certaine façon, un conteneur de pointeur à un seul élément (ie une capsule), tu trouves étrange de pouvoir accéder à cet élément ? Pourtant accéder aux éléments d'un conteneur ne doit pas te choquer ?
    Les typename, je m'en sers déjà en dehors des unique_ptr en fait. C'est bien pratique pour éviter de changer un type 20 000 fois dans le code alors que l'interface est identique.
    Mais les typename n'empêchent pas de devoir faire appel aux fonctions membres.
    Hier j'ai enlevé tous les unique_ptr de mon code, ça m'a pris pas mal de temps. Bien plus que si je m'étais amusé à convertir les vector en tableaux en durs (surtout avec la nouvelle écriture de for)
    Ce qui fait que je considère vector (par exemple) comme un progrès réel, je ne le retrouve clairement pas dans unique_ptr: lisibilité, sécurité, adaptabilité du code.

    Je ne trouve pas illogique d'accéder aux éléments d'un conteneur, mais on ne peux y accéder que de façon protégée. On ne peux pas, à ma connaissance, casser une string en jouant avec les méthodes qu'elle offre (bien qu'il soit possible de faire buguer le code utilisateur en voulant modifier le retour de c_str()). Le conteneur unique_ptr, on peut tout faire crasher, et ce, relativement facilement.
    Il suffit de faire un truc du genre "delete myUnique.get();" pour vérifier que unique_ptr ne garantit PAS qu'il est le seul à posséder la responsabilité.
    Ce problème aurait pû être évité si au lieu de renvoyer un raw ptr la méthode get() avait renvoyé un readonly_ptr.

    D'ailleurs, merci beaucoup pour le lien, à lire le paragraphe "motivation" j'ai l'impression que ça va pas mal me servir.

    Citation Envoyé par Flob90 Voir le message
    C'est bien moins flexible (et c'est redondant), l'utilisation des pointeurs intelligents (plus exactement : de capsule RAII non invasive), permet d'avoir un contrôle plus fin sur les points de variation du design. Avec ta technique si tu veux changer la gestion de la ressource tu te retrouves à modifier ta classe représentant la ressource, avec une capsule style std::unique_ptr (et une bonne utilisation des typename), tu as juste à changer une ligne totalement extérieur à la classe représentant ta ressource.
    Pour les cas ou il y a polymorphisme, c'est vrai que cette méthode n'est pas du tout optimale. Mais quand ce n'est pas le cas, il n'y a pas qu'un typedef à modifier, mais aussi tous les appels de fonction nécessitant ce pointeur.
    Des exemples où simplement utiliser unique_ptr n'offre aucune souplesse comparé à cette méthode, c'est l'usage de la SDL (écrite en C) ou de wxWidgets (C++, mais avec une certaine dose de C with classes tout de même).
    Pour la SDL, l'usage des pointeurs est évident, puisque c'est du C (notamment pour la vidéo). Pour wxWidgets, nettement moins, cependant certaines parties sont assez bancales et obligent à une manipulation mémoire assez laide (je pense au mécanisme qui gère les menus, notamment. Oh et puis non, en fait, il y a des pointeurs dans tous les sens, accompagnés de ce que je qualifierais de problèmes de conception).
    Dès lors que l'on utilise une librairie tierce, on sait que la structure ne changera pas tout le temps, et encapsuler soi-même les pointeurs imposés permet une meilleure abstraction. En fait, ça permet de changer de lib en peu de temps, sans changer les interfaces publiques. Ce que unique_ptr ne permet pas (ce n'est pas non plus son rôle).

    Citation Envoyé par Flob90 Voir le message
    Regardes les std::reference_wrapper, tu pourras les mettre dans les conteneurs. Pour pointeur/référence, il est en effet probable qu'en interne l'implémentation des références et des pointeurs soit similaires car les processeurs ne doivent pas offrir de quoi faire une distinction. Par contre d'un point de vue C++ (ie la façon dont le langage les définit) la différence existe : un pointeur est un objet (au sens du modèle de la mémoire en C++) alors qu'une référence n'est qu'une variable et pas un objet. D'un point de vue sémantique, un pointeur a une sémantique de référence (dans le sens où elle peut servir à référer) mais pas seulement (ils ont une notion d'arithmétique), alors qu'une référence C++ a seulement la sémantique de référence.
    Oui, c'est même pour ça que je n'ai rien contre les pointeurs, je sais que l'on peut jouer avec de la même façon qu'un tableau et je pense qu'ils sont également plus rapides que les itérateurs des conteneurs standards.
    Si un jour j'ai besoin d'optimiser la vitesse d'accès à des données, nul doute que je les utiliserais.
    Mais à mon humble avis, obliger l'utilisation des pointeurs pour qu'un conteneur contienne des objets polymorphes est regrettable (a voir pour reference_wrapper cela dis, je n'en avais jamais entendu parler) et même dangereux (risque de pointeur nul qui vient d'on ne sait où parce qu'utilisé 50 ans après avoir été altéré, problèmes d'allocations...).
    Bon, je suppose qu'il y a un problème technique derrière cette restriction.

    Citation Envoyé par Flob90 Voir le message
    Edit: Je ne pense pas non plus que tout code doit nécessairement contenir des std::unique_ptr/boost::scoped_ptr, ca dépend du code. Par contre s'en passer là où il est approprié est, AMA, une erreur. Pour la sérialisation, j'y connais pas grand chose, cependant ce n'est pas la partie d'un design où le besoin de pointeurs intelligents me parait évident : l'important pour cet élément ce sont les données, pas qui en a la responsabilité, sérialiser les capsule-RAII est-il nécessaire ?
    La sérialisation implique que lorsque le programme rencontre un pointeur, il sérialise aussi son contenu, et ce, de façon à respecter le fait qu'il s'agisse d'un pointeur, pas d'une instance.
    De même, la sérialisation doit accéder à la plupart des membres d'une classe.
    Ce qui fait qu'il n'y a pas 30 000 solutions: soit on est intrusifs (on modifie le code de la classe, ne serait-ce que pour ajouter un ami) et on garde un code propre (avec des membres privés donc), soit on colle tout le monde en public, ou plus probablement, on donne accès à des méthodes permettant d'interroger et d'écrire l'état de chaque membre (ce qui reviens plus ou mois au même, avec 2-3 sécurités en plus)
    Conséquence: si il y a des unique_ptr dans les membres, il faut pouvoir s'en occuper comme tels.
    Cela dis, je ne suis pas non plus un expert. C'est juste que le besoin s'est fait sentir sur mon projet d'avoir un système de sauvegarde/chargement et que du coup j'ai cherché un peu et ai fini par aboutir sur boost::serialization, qui à le mérite de pouvoir le faire de façon portable, de façon à conserver la compatibilité des fichiers entre les versions (très important pour un projet qui utilise massivement des plugin ça) et de pouvoir implémenter le format (ou type de format) que l'on veut sans modifier le code à sérialiser.



    Citation Envoyé par JolyLoic Voir le message
    Juste une réponse en passant sur un point. Autant pour un shared_ptr, accéder au pointeur contenu ne doit avoir lieu que pour de l'interfaçage avec du vieux code, autant pour un unique_ptr, je pense que ça peut intervenir plus souvent.

    En effet, côté shared, on a un couple shared_ptr/weak_ptr. Côté unique, le même couple s'écrit aujourd'hui unique_ptr/T* (et je n'ai pas réussi à vraiment convaincre les gens qu'une telle écriture n'est pas idéale).

    Il y a donc, en collaboration avec unique_ptr la possibilité tout à fait légitime et justifiée d'avoir des T* qui représentent des pointeurs n'influençant pas la durée de vie de l'objet pointé, et agissant en simples observateurs de l'unique_ptr (il faut bien entendu que l'architecture du programme permette de s'assurer que ce dernier restera vivant tant que l'observateur peut être utilisé). Et le moyen d'obtenir un T* à partir d'un unique_ptr, c'est justement get.

    Je pense que la règle à la louche n'est donc pas de faire attention aux get, ni aux pointeurs nus. Mais de faire attention aux delete. Un delete dans du code moderne, sauf s'il est dans l'implémentation d'un wrapper type smart pointer, et probablement un code smell. Pourquoi doit-on le faire manuellement, et ne risque-t-on pas de marcher sur les plates-bandes d'un autre mécanisme de gestion de la mémoire.
    Entièrement d'accord sur le fait que cette écriture n'est pas idéale. Je me permettrais même de dire que c'est une écriture dangereuse, qui favorise ce que tu appelles du code smell.
    unique_ptr me donne l'impression de ne pas être fini au final. C'est quand même marrant que le comité ait mieux protégé un outil qui devrait être utilisé de façon plus restreinte (shared_ptr qui permet à plusieurs d'avoir la responsabilité d'un objet) qu'un outil qui semble avoir une vocation à un usage plus répandu...
    Limite je trouve shared_ptr plus élégant...

  8. #8
    Membre Expert

    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 : 35
    Localisation : France, Doubs (Franche Comté)

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

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Par défaut
    Citation Envoyé par Freem Voir le message
    Désolé.
    Le souci de ton exemple simple, c'est qu'il l'est trop.
    Je veux dire, il est tellement simple qu'on ne voit pas véritablement l'intérêt d'utiliser unique_ptr, excepté le fait de ne pas avoir à implémenter de destructeur.
    Du coup, je n'ai pas grand chose à dire à son sujet
    Tu viens de dire exactement ce qu'il fallait Non l'utilisation de unique_ptr ici ne permet pas seulement d'avoir à ne pas écrire le destructeur, si tu penses ca alors je te propose d'écrire ce même code "simple" sans unique_ptr pour te rendre compte des problèmes (si tu ne vois aucun problème, alors postes le code, on pourra en discuter ).

    Citation Envoyé par Freem Voir le message
    Les typename, je m'en sers déjà en dehors des unique_ptr en fait. C'est bien pratique pour éviter de changer un type 20 000 fois dans le code alors que l'interface est identique.
    Mais les typename n'empêchent pas de devoir faire appel aux fonctions membres.
    Hier j'ai enlevé tous les unique_ptr de mon code, ça m'a pris pas mal de temps. Bien plus que si je m'étais amusé à convertir les vector en tableaux en durs (surtout avec la nouvelle écriture de for)
    Ce qui fait que je considère vector (par exemple) comme un progrès réel, je ne le retrouve clairement pas dans unique_ptr: lisibilité, sécurité, adaptabilité du code.
    J'indiquais l'utilisation des typename en réaction à la partie de ton message qui disait que ça faisait du code à rallonge d'utiliser des unique_ptr dans un conteneur, c'est bien uniquement le type qui est rallongé, pas les appels de fonctions, ou alors je n'ai pas compris ce que tu disais.

    Citation Envoyé par Freem Voir le message
    Je ne trouve pas illogique d'accéder aux éléments d'un conteneur, mais on ne peux y accéder que de façon protégée. On ne peux pas, à ma connaissance, casser une string en jouant avec les méthodes qu'elle offre (bien qu'il soit possible de faire buguer le code utilisateur en voulant modifier le retour de c_str()). Le conteneur unique_ptr, on peut tout faire crasher, et ce, relativement facilement.
    Il suffit de faire un truc du genre "delete myUnique.get();" pour vérifier que unique_ptr ne garantit PAS qu'il est le seul à posséder la responsabilité.
    Ce problème aurait pû être évité si au lieu de renvoyer un raw ptr la méthode get() avait renvoyé un readonly_ptr.
    Je pense que la décision première du comité pour get c'est le besoin de retro-compatibilité, donc le type de retour sera nécessairement un pointeur nue au final. Je suis d'accord avec Loic sur le fait qu'une autre capsule aurait été un meilleur choix pour réaliser le couple unique_ptr/T* qu'un pointeur nue, mais le besoin de retro-compatibilité impose le besoin de pouvoir avoir un pointeur nue.

    Au final, oui il manque peut-être quelque chose. Ça impose à l'utilisateur une certaine prudence (deux services regroupés sous le même mécanisme). Cependant la distinction entre les deux services en normalement assez clair pour que la confusion maladroite soit difficile (en général tu sais quand même si tu écris une portion de code qui a un besoin de retro-compatibilité ou non), c'est plutôt la verbosité du code qui en pâtit.

    Citation Envoyé par Freem Voir le message
    Dès lors que l'on utilise une librairie tierce, on sait que la structure ne changera pas tout le temps, et encapsuler soi-même les pointeurs imposés permet une meilleure abstraction. En fait, ça permet de changer de lib en peu de temps, sans changer les interfaces publiques. Ce que unique_ptr ne permet pas (ce n'est pas non plus son rôle).
    Je ne vois pas en quoi l'abstraction est meilleurs, je n'ai pas dit de ne pas réaliser d'interface permettant une abstraction des bibliothèques utilisés en implémentation, mais de laisser le rôle de la gestion de la durée de vie à la capsule, c'est ce que la capsule encapsule qui doit offrir l'abstraction des détails d'implémentation. Dans les deux cas l'abstraction est la même, la différence est que dans un cas les rôles sont clairs, une classes un rôle (unique_ptr : gestion de la durée de vie, ta classe : abstraction de l'implémentation, rôle précis définie selon le contexte), dans l'autre ta classe fait deux choses.

    Citation Envoyé par Freem Voir le message
    Oui, c'est même pour ça que je n'ai rien contre les pointeurs, je sais que l'on peut jouer avec de la même façon qu'un tableau et je pense qu'ils sont également plus rapides que les itérateurs des conteneurs standards.
    Si un jour j'ai besoin d'optimiser la vitesse d'accès à des données, nul doute que je les utiliserais.
    Mais à mon humble avis, obliger l'utilisation des pointeurs pour qu'un conteneur contienne des objets polymorphes est regrettable (a voir pour reference_wrapper cela dis, je n'en avais jamais entendu parler) et même dangereux (risque de pointeur nul qui vient d'on ne sait où parce qu'utilisé 50 ans après avoir été altéré, problèmes d'allocations...).
    Bon, je suppose qu'il y a un problème technique derrière cette restriction.
    Le paragraphe que j'ai fait n'est surement pas clair, mais pour faire simple, une référence n'a aucune notion d'espace mémoire (contrairement à un pointeur) dans la description du C++, ce n'est pas une entité physique en mémoire. Or l'objectif des conteneurs est de "contenir" des entités physique en mémoire (des objets), donc par définition pas des références.

Discussions similaires

  1. Réponses: 52
    Dernier message: 23/10/2014, 11h22
  2. Réponses: 19
    Dernier message: 29/09/2014, 17h12
  3. Réponses: 31
    Dernier message: 19/09/2014, 20h40
  4. boost::serialization et std::unique_ptr
    Par Freem dans le forum Boost
    Réponses: 7
    Dernier message: 09/08/2012, 15h08

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