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. #61
    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 Médinoc Voir le message
    Mais qui a dit que cette logique était bonne pour Qt?
    Au choix:
    1. Parce que c'est une alternative plus que correcte à la nécessité de disposer d'un "gestionnaire global centralisé" pour les différents objet
    2. Parce qu'elle ne fait qu'appliquer ce que l'on répète sans cesse aux débutants : chaque classe doit être responsable de ressources qu'elle manipule
    3. Parce que cela fonctionne très bien comme cela
    4. Un peu les trois à la fois
    Et, surtout, j'aurais envie de te retourner la question : pourquoi estimerais tu, même à l'heure actuelle où l'on dispose des pointeurs intelligents, que cette logique n'était pas la bonne, et par quoi la remplacerais tu
    L'interface de Qt est née avant C++11, donc avant la sémantique de mouvement "done right". C'est donc non pas par choix, mais par circonstance (pour ne pas dire nécessité) que Qt n'a pas d'unique_ptr<> ou équivalent potable dans son interface.
    Tout dépend de la manière dont tu envisages les pointeurs intelligents :
    Personnellement, je ne les envisage que pour ce qu'ils sont : des classes qui permettent
    • de prendre de manière cohérente la libération des ressources en charge au moment de la destruction, et de respecter le RAII.
    • Des classes qui permettent à peu de frais d'augmenter la résistance aux exceptions

    La sémantique de mouvement est apparue, selon moi, pour d'autres raisons, comme le fait de vouloir être en mesure de favoriser (N)RVO chaque fois que faire se peut et, effectivement, de permettre de considérer comme légitime le fait qu'un développeur puisse vouloir faire en sorte "d'abandonner" le contenu d'un objet non copiable au profit d'un autre objet de même type.

    Bien sur, la sémantique de mouvement se marie très bien avec les pointeurs intelligents. Bien sur, la présence de ces deux notions que j'aurais tendance à qualifier d'orthogonales permet au développeur de forcer l'utilisation de pointeurs intelligents et de la sémantique de mouvement.

    Mais l'optique de C++ a toujours été claire en la matière et se limite à une phrase simple "trust developers even if its bullshits".

    Dés lors, les questions que je tente de faire en sorte que vous vous posiez sont:
    • Est-ce que cela a du sens de vouloir forcer l'utilisateur à adopter cette approche
    • Est-ce qu'il n'est pas plus utile d'adopter cette approche de manière globale (au projet, au module) que d'essayer de le faire dans un cas bien particulier qui se placerait en opposition par rappor à l'ensemble global (projet, module)

    Mon approche personnelle est que l'on a "tout à gagner" à envisager unique_ptr comme étant un détail d'implémentation, utilisé de manière généralisée mais silencieuse au niveau du projet ou du module.

    Et je me sens d'autant plus à l'aise avec cette position que c'est en gros ce que disait Stephan Lavajef lors des going native.
    Edit: De plus, tu sembles être revenu en arrière, au principe de "fuite d'info sur l'implémentation" que je croyais déjà démenti et loin derrière nous...
    En effet, mais le fait est que je n'ai toujours pas été convaincus par les arguments exposé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

  2. #62
    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 koala01 Voir le message
    Mon approche personnelle est que l'on a "tout à gagner" à envisager unique_ptr comme étant un détail d'implémentation, utilisé de manière généralisée mais silencieuse au niveau du projet ou du module.
    A nouveau tes arguments ressemblent furieusement à ceux que j'ai entendu il y a 15 ans contre std::string dans les interfaces. Il y en a d'ailleurs qui tiennent toujours ces raisonnements, mais leur nombre a bien décru. Mais bon, au contraire des std::string, les cas où j'ai des interfaces publiques qui permettent de donner la responsabilité de la libération d'un pointeur à un module sont tellement rares que je ne vois pas la peine d'en discuter tant que tu admets que retourner un unique_ptr est la chose à faire quand une fonction transfère la responsabilité à son appelant.

    Et je me sens d'autant plus à l'aise avec cette position que c'est en gros ce que disait Stephan Lavajef lors des going native.
    Entre ce que Luc cite et ta conclusion, il y a un raisonnement qui m'échappe.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  3. #63
    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
    Il me semble souvenir que Qt n'est absolument pas exception-safe, me trompè-je ?

  4. #64
    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 Jean-Marc.Bourguet Voir le message
    A nouveau tes arguments ressemblent furieusement à ceux que j'ai entendu il y a 15 ans contre std::string dans les interfaces.
    Il y a quand même une différence de taille entre vector/string et pointeurs intelligents :

    Prenons deux situations qui, selon toi, seraient fort semblables:
    un projet dans lequel string/vector ne sont jamais utilisés et un deuxième projet dans lequel les pointeurs intelligents ne le seraient pas d'avantage.

    Dans le premier projet, tu décides de remplacer le char * ou le type * d'une des classes par std::string ou par std::vecto<T>.

    Hé bien, tu le fais, et tu n'as, à peu de choses près, pas à t'inquiéter du reste!

    Il y aura bien l'une ou l'autre fonction extérieure à ta classe qu'il faudra reprendre, mais, avec un peu de chance, cela restera limité à ce que je vais appeler "les alentours immédiats" de la classe que tu auras modifiée.

    Prenons maintenant le deuxième projet, et modifions également l'une des classes afin qu'elle manipule un pointeur intelligent. C'est là que les problèmes vont commencer à arriver.

    Ah, il n'y a pas de problème, la classe modifiée sera beaucoup plus sécurisante à l'emploi, ca, je n'en disconviens absolument pas!

    Mais le fait est que tu ne pourras pas te "limiter" à la seule modification de cette classe, car, autrement, tu vas te retrouver avec des références non comptées pour tes shared_ptr ou avec des classes et des fonctions qui, manque de pot, manipulent justement le pointeur qui fini sous la responsabilité de ton unique_ptr et auxquelles il te sera impossible de signaler que le pointeur qu'elles manipulent est invalidé.

    Au final, là où tu peux te "contenter" d'une approche très restreinte avec string / vector, tu es obligé d'avoir une approche tout à fait globale du problème avec les pointeurs intelligents.

    Maintenant, ne vas pas me faire dire ce que je n'ai pas dit : je ne dis absolument pas qu'il ne faut pas utiliser les pointeurs intelligents, au contraire, je suis d'avis qu'il faut en user et en abuser.

    Mais je suis d'avis qu'il faut les utiliser dans une approche globale du problème, c'est à dire que, si une classe peut prendre la responsabilité de la durée de vie d'un pointeur, hé bien, elle présente des comportements qui permettent de la relever de cette responsabilité "mais attention, je m'en lave les mains" et/ou de décider à un moment donné de détruire le pointeur dont elle a la responsabilité et / ou de prêter le pointeur "mais attention, je le détruis quand je suis détruit" et / ou de prendre la responsabilité du pointeur "et maintenant, c'est moi qui m'en charge et personne d'autre".
    (tous ces comportements étant possibles, non obligatoires et non exclusifs évidemment )

    Mais, que la classe manipule en interne un std::unique_ptr, un boost::unique_ptr ou une implémentation personnelle similaire, l'utilisateur n'a pas plus besoin de le savoir (au niveau de ce qui apparait dans son interface s'entend) qu'il n'a besoin de savoir (au niveau de l'interface toujours) que la classe Matrice qu'il utilise un tableau une dimension de colonnes * lignes éléments, de lignes * colonnes élément ou un tableau à deux dimensions

    Il y en a d'ailleurs qui tiennent toujours ces raisonnements, mais leur nombre a bien décru. Mais bon, au contraire des std::string, les cas où j'ai des interfaces publiques qui permettent de donner la responsabilité de la libération d'un pointeur à un module sont tellement rares que je ne vois pas la peine d'en discuter tant que tu admets que retourner un unique_ptr est la chose à faire quand une fonction transfère la responsabilité à son appelant.
    Même cela je n'en suis pas convaincu, pour autant qu'il est clair -- quelle que soit la manière envisagée pour le rendre clair -- que le pointeur renvoyé est "libre de toute attache"
    Entre ce que Luc cite et ta conclusion, il y a un raisonnement qui m'échappe.
    Je vais donc citer le passage qui m'intéresse
    * N'utilisez pas move() pour retourner une variable locale par valeur
    -> la NRVO et la sémantique de déplacement sont conçues pour travailler main dans la main
    -> quand la NRVO est applicable, la construction directe est optimale
    -> dans le cas contraire, la sémantique de déplacement est efficace (et activée donc)

    * Ne retournez pas par rvalue reference
    -> pour les experts seulement, besoin rare
    -> Même le comité de standardisation s'est brulé les ailes
    -> quelques exemples valides toutefois : forward, move, declval, get(tuple&&)
    et préciser ma conclusion : dans la majorité des cas, le compilateur saura très bien s'il doit recourir ou non à la sémantique de mouvement.

    Dés lors, pourquoi faudrait il commencer à exposer un détail d'implémentation qui, en plus de poser certains problèmes (sur lesquels je crois m'être assez étendu), te fera en plus te poser éternellement la question de savoir si tu dois plutôt envisager le passe par valeur ou par rvalue-reference
    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

  5. #65
    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
    Il me semble souvenir que Qt n'est absolument pas exception-safe, me trompè-je ?
    Oui, quand même un tout petit peu...

    Au niveau interne, les pointeurs manipulés par Qt sont tous des pointeurs intelligents, même si c'est une implémentation "maison"
    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. #66
    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
    En tous les cas pour ma part les pointeurs sont (depuis peu je l'avoue) un détail d'implémentation que je ne vais justement plus prendre en paramètre, que ce soit nu ou intelligent. (je vais suivre le conseil de ce cher Bjarne Stroustrup )
    Et je ne m'en porte que mieux, à part des problèmes ça n'amène pas grand chose.

  7. #67
    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
    @koala01: La conclusion que tu cites concerne les retours, pas les passages. Ça n'a absolument rien à voir. En effet le compilateur sait, bien souvent, jouer avec la move tout seul dans ce cas car il s'agit d'une sortie de portée des variables locales retournées, quand on est dans un passage, hors le cas des temporaires, il ne saura pas jouer avec seul.

    Pour le reste, je ne crois pas que l'on parlait de shared_ptr, si ? Parce que si on ne parle pas de shared_ptr, la comparaison char*/string et T*/unique_ptr est parfaite : les deux offre un strict ownership sur une ressource alloué dynamiquement. Et par conséquent les modifications à faire sont de la même nature.

    D'autre part, j'ai du mal à voir l'intérêt d'une telle comparaison. En effet il faut modifier le code pour passer de raw_ptr à smart_ptr, et alors ? Si c'est pour avoir un code plus à même d'assurer que chaque ressource est bien gérée, je ne vois pas où est le problème. On briserait la compatibilité avec de l'ancien code, je veux bien considéré que c'est problématique, mais là ce n'est pas le cas, il est toujours possible de l'utiliser.

  8. #68
    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 Flob90 Voir le message
    Pour le reste, je ne crois pas que l'on parlait de shared_ptr, si ? Parce que si on ne parle pas de shared_ptr, la comparaison char*/string et T*/unique_ptr est parfaite : les deux offre un strict ownership sur une ressource alloué dynamiquement. Et par conséquent les modifications à faire sont de la même nature.
    Encore une fois, il y a une très grosse différence de scope entre ta std::string (ou ton std::vector<T>) et ton pointeur intelligent :

    std::string et std::vector vont travailler au niveau d'une fonction, au niveau d'une classe et dans le pire des cas au niveau de quelques fonctions ou de quelques classe connexes.

    Si tu as une centaine de classes qui n'utilisent pas std::string (alors qu'elles devraient le faire) et que tu modifies une de ces classe "au hasard" pour qu'elle manipule effectivement une std::string, tu ne devras reporter cette modification que dans un nombre particulièrement restreint d'autres classes.

    Dans les mêmes circonstances, tu décides de modifier une classe qui ne manipule pas un pointeur intelligent afin de la faire manipuler ce type de pointeur, c'est toutes les classes que tu devras modifier afin de prendre ce changement en compte!
    D'autre part, j'ai du mal à voir l'intérêt d'une telle comparaison. En effet il faut modifier le code pour passer de raw_ptr à smart_ptr, et alors ? Si c'est pour avoir un code plus à même d'assurer que chaque ressource est bien gérée, je ne vois pas où est le problème.
    Bien sur que c'est pour un mieux, je n'ai jamais dit le contraire!

    Mais, en attendant, si tu "oublies" de modifier une seule classe (et tu peux t'attendre à en oublier bien d'avantage), tu vas passer des heures à traquer l'endroit où la (les) classes que tu as oublier de modifier provoquent l'erreur de segmentation qui apparait de manière récurrente.
    On briserait la compatibilité avec de l'ancien code, je veux bien considéré que c'est problématique, mais là ce n'est pas le cas, il est toujours possible de l'utiliser.
    La compatibilité n'est encore qu'un faux problème à partir du moment où c'est pour un mieux (enfin, dans certains cas, ca peut quand meme être un problème des plus sérieux, mais bon, faisons comme si )

    Il ne s'agit pas de remettre en question le bien-fondé de la décision d'utiliser les pointeurs intelligents, je l'ai dit et je le redis : il faut les utiliser au maximum, en user et en abuser.

    Mais ce qu'il s'agit de remettre en question, c'est l'approche que l'on peut avoir des pointeurs intelligents :

    Passer de char* à std::string, cela peut se faire au départ d'une approche très limitée (une classe et quelques fonctions connexes).

    Passer de pointeurs nus à des pointeurs intelligents, cela ne peut se faire que dans le cadre d'une approche globale, de l'ensemble du module ou du projet sur lequel tu travailles.

    Si tu n'as pas cette approche globale, j'ai envie de dire, si tu ne dresses pas la liste exhaustive de tous les points qui seront impactés par le changement, tu peux t'attendre à faire sauter la majeure partie de tes tests unitaires et / ou à crouler sous les rapports de bugs suite à autant d'erreurs de segmentation dans autant de circonstances différentes!

    Ou, si tu préfères :

    Je suis on ne peut plus conscient du bénéfice qu'il y a à passer de la gestion "manuelle" des pointeurs nus au pointeurs intelligents , quels qu'ils soient, et je suis même conscient du fait qu'il y aura beaucoup plus de bénéfices à le faire qu'il ne pourrait y avoir à passer d'un char * à une std::string dans une classe.

    Mais il faut être conscient que la somme de travail pour arriver à le faire correctement est très largement supérieure à la somme de travail qu'il faudrait fournir pour même faire passer toutes les classes de l'utilisation de char* à l'utilisation de std::string, parce que là où le passage à std::string n'implique que des modifications très localisées, le passage aux pointeurs intelligents implique des modifications en profondeurs globalisées et ce, quelle que soit la manière dont on envisage les choses.

    Et c'est pour cela que je dis que l'utilisation d'un pointeur intelligent est et doit rester un détail d'implémentation, qu'on n'a pas à exposer plus que de raison (autrement dit : en fournissant les services qui vont bien pour prêter / libérer / changer le pointeur géré par unique_ptr ou en fournissant une interface basée sur weak_ptr si la classe gère un shared_ptr en interne) le fait que l'on travaille avec des pointeurs intelligents (quels qu'ils soient), que c'est une information qui doit circuler de manière globale au projet, au travers de règles de codage, au travers de la documentation générale et non au travers de l'argument spécifique à une fonction.

    Le tout n'a, en définitive, qu'un seul but : faciliter au maximum la vie du développeur (qui devra résoudre les problèmes de l'utilisateur) et de l'utilisateur :
    Pour l'utilisateur :
    • il n'a pas à s'inquiéter des pointeurs nus , ils sont d'office gérés "par ailleurs"
    • s'il veut prendre la responsabilité, il passe par un comportement spécifique : release
    • s'il veut forcer la destruction (oui, ca arrive), il passe par un autre comportement spécifique : reset
    • s'il veut transférer la responsabilité, passe par un troisième (ou peut etrre un quatrième, s'il veut échanger les responsabilités) comportement
    (je reviens ici sur le cas spécifique unique_ptr, tu l'auras compris )

    Et pour le développeur, il voit tout de suite si le code semble correct ou non. Avec un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    b.takeResponsability(a.get());
    il pourra tout de suite dire "oui, mais mon grand, tu demandes à b de prendre la responsabilité, mais tu demandes à a de te prêter le pointeur tout en en gardant la responsabilité... tu t'étonnes que ca foire "

    Tu me diras, et je reconnais volontiers que c'est vrai, que le développeur pourra tout aussi bien repérer
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    b.takeResponsability(std::move(a.foo());
    Mais, la première version me semble malgré tout "plus naturelle" (en plus d'éviter au développeur d'avoir à se poser la question du passage par valeur, par référence ou par rvalue-reference)

    Après, bien sur, on peut parler des quelques cas qui ne manqueront pas de survenir dans lesquels une fonction (ou une classe) qui ne prend pas la responsabilité du pointeur ferait appel à une fonction (ou à une classe) qui veut la prendre.

    Mais ce problème là, nous y serons confrontés dans les deux situations
    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. #69
    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 koala01 Voir le message
    Il y a quand même une différence de taille entre vector/string et pointeurs intelligents :

    Prenons deux situations qui, selon toi, seraient fort semblables:
    un projet dans lequel string/vector ne sont jamais utilisés et un deuxième projet dans lequel les pointeurs intelligents ne le seraient pas d'avantage.

    Dans le premier projet, tu décides de remplacer le char * ou le type * d'une des classes par std::string ou par std::vecto<T>.

    Hé bien, tu le fais, et tu n'as, à peu de choses près, pas à t'inquiéter du reste!

    Il y aura bien l'une ou l'autre fonction extérieure à ta classe qu'il faudra reprendre, mais, avec un peu de chance, cela restera limité à ce que je vais appeler "les alentours immédiats" de la classe que tu auras modifiée.

    Prenons maintenant le deuxième projet, et modifions également l'une des classes afin qu'elle manipule un pointeur intelligent. C'est là que les problèmes vont commencer à arriver.

    Ah, il n'y a pas de problème, la classe modifiée sera beaucoup plus sécurisante à l'emploi, ca, je n'en disconviens absolument pas!

    Mais le fait est que tu ne pourras pas te "limiter" à la seule modification de cette classe, car, autrement, tu vas te retrouver avec des références non comptées pour tes shared_ptr ou avec des classes et des fonctions qui, manque de pot, manipulent justement le pointeur qui fini sous la responsabilité de ton unique_ptr et auxquelles il te sera impossible de signaler que le pointeur qu'elles manipulent est invalidé.
    On n'est vraiment pas d'accord. Tout d'abord, on n'a jamais parlé de shared_ptr jusqu'à présent. Mais bon, si tu veux le traiter.

    Reprenons les bases. Il y a différentes politiques de gestion de la mémoire. Certaines sont locales, elles ne demandent comme collaboration entre composants participants que l'accord sur la politique à mener et sur la définition l'interfaces, le moyen de la mettre en œuvre reste d'un choix purement local. C'est le cas de la responsabilité non partagée. D'autres sont globales, elles nécessitent non seulement l'accord sur la politique et l'interface, mais aussi sur le moyen de la mise en œuvre. C'est le cas de la responsabilité partagée.

    Dans le premier cas, un composant peut décider seul du moyen de mettre en œuvre sa politique, quel que soit son choix pour l'implémentation de la politique, il peut le modifier tant qu'il ne touche pas son interface sans que ça touche les autres. Et s'il touche son interface, ça ne va toucher ses partenaires que dans les alentours immédiats pour reprendre ta formulation. Et c'est vrai pour unique_ptr.

    Dans le second choix, une décision de changer le moyen de mettre en œuvre la politique va devoir toucher tout le monde, puisqu'il faut collaborer sur cette mise en œuvre ailleurs qu'aux interfaces.

    Quand tu écris:

    Au final, là où tu peux te "contenter" d'une approche très restreinte avec string / vector, tu es obligé d'avoir une approche tout à fait globale du problème avec les pointeurs intelligents.
    C'est vrai aussi pour unique_ptr, qui était l'unique objet de la discussion jusqu'à présent. Et mon point est qu'unique_ptr est devenu le moyen standard de gérer la politique "possesseur unique", tout comme std::string est devenu le moyen standard de gérer les chaines de caractères, n'en déplaise à tout ceux qui, Qt en tête puisque tu le citais, gardent leur propre type de chaines pour de plus ou moins bonnes raisons, la compatibilité en est une bonne, l'attachement fétiche tenant de NIH en est une mauvaise. Pour les chaines comme pour les pointeurs, quand j'ai de bonnes raisons de les manipuler autrement que de la manière standard, je le fais entre mes sous-composants, je ne l'expose pas dans mon interface. Parce que plus le temps va passer, tout comme il est devenu de plus en plus surprenant de voir des char const* ou des nihString dans les interfaces, de plus il va être surprenant de voir des T* ou des nihUniquePtr<T> dans celles-ci. Les nihCountedPtr<T> vont eux rester plus longtemps, exactement pour la raison que tu donnes, c'est un choix non local et le basculement devra être fait d'un coup et pas par percolation à partir de noyaux précurseurs.

    Mais je suis d'avis qu'il faut les utiliser dans une approche globale du problème, c'est à dire que, si une classe peut prendre la responsabilité de la durée de vie d'un pointeur, hé bien, elle présente des comportements qui permettent de la relever de cette responsabilité "mais attention, je m'en lave les mains" et/ou de décider à un moment donné de détruire le pointeur dont elle a la responsabilité et / ou de prêter le pointeur "mais attention, je le détruis quand je suis détruit" et / ou de prendre la responsabilité du pointeur "et maintenant, c'est moi qui m'en charge et personne d'autre". (tous ces comportements étant possibles, non obligatoires et non exclusifs évidemment )
    Tu n'as toujours pas montré en quoi unique_ptr ne convenait pas pour ça.

    Je vais donc citer le passage qui m'intéresse
    et préciser ma conclusion : dans la majorité des cas, le compilateur saura très bien s'il doit recourir ou non à la sémantique de mouvement.
    Il dit deux choses:
    - return std::move(varlocale); est à éviter car return varlocale; est au moins aussi efficace. Personne n'a écrit le contraire. Je ne vois pas en quoi ça déconseille d'avoir unique_ptr<T> comme type de retour.
    - X&& f() est à éviter sauf cas très technique. Ça ne dit rien sur unique_ptr<T> f() qu'on préconise.

    Dés lors, pourquoi faudrait il commencer à exposer un détail d'implémentation qui, en plus de poser certains problèmes (sur lesquels je crois m'être assez étendu), te fera en plus te poser éternellement la question de savoir si tu dois plutôt envisager le passe par valeur ou par rvalue-reference
    En dehors des templates pour les passer après avec std::forward, je ne vois pas d'utilité aux rvalue-reference en paramètres.

    Je questionnerais même plutôt fortement l'intérêt d'une prise conditionnelle de la responsabilité, et si elle est réellement nécessaire, c'est tellement atypique que je ne veux pas qu'elle soit marquée syntaxiquement à l'appel de la même façon qu'un transfert inconditionnel.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  10. #70
    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
    @Jean-Marc.Bourguet: Je suis globalement d'accord avec, du moins sur l'ensemble des points que tu opposes à Koala01. Pour les autres j'ai quelques réserves :

    • de plus il va être surprenant de voir des T* ou des nihUniquePtr<T> dans celles-ci
      Pour ceci, je m'interroge sur la possibilité d'avoir des classes ayant une sémantiques identiques à std::unique_ptr mais des variations sur certains points. Si je prends la classe SmartPtr de Loki par exemple, on peut avoir différentes instances (template) de celle-ci ayant la même politique d'Ownership (et donc des variations des autres politiques). Cependant c'est une question dont la réponse m'intéresse assez peu, au besoin il suffit d'introduire un Concept StrictOwnership et une fonction pourra accepter ce qu'on veut (en C++14 / C++17).
    • En dehors des templates pour les passer après avec std::forward, je ne vois pas d'utilité aux rvalue-reference en paramètres.

      Je questionnerais même plutôt fortement l'intérêt d'une prise conditionnelle de la responsabilité, et si elle est réellement nécessaire, c'est tellement atypique que je ne veux pas qu'elle soit marquée syntaxiquement à l'appel de la même façon qu'un transfert inconditionnel.
      Cella rejoint le point de vue que j'exprimais plutôt avec white_tentacles, si tu considères les exceptions et que la résistance forte est de revenir à l'état avant l'évaluation de l'expression contenant ta fonction et que tu l'appliques, alors tu es systématiquement dans un cas conditionnel (*).

      Par contre c'est aussi le seul cas non-atypique de prises conditionnelles que j'envisage, toutes les autres devraient être indiquées d'une autre façon (**).


    (*) Sauf en situation noexcept, et j'ai tendance à supposer qu'on est pas en noexcept de manière général (et c'est toujours ce qu'on fait quand on parle d'écriture idiomatique et de bonnes habitudes en général).

    (**) Passage par référence constante et documentation.

  11. #71
    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 Flob90 Voir le message
    @Jean-Marc.Bourguet: Je suis globalement d'accord avec, du moins sur l'ensemble des points que tu opposes à Koala01. Pour les autres j'ai quelques réserves :

    [LIST][*]
    Pour ceci, je m'interroge sur la possibilité d'avoir des classes ayant une sémantiques identiques à std::unique_ptr mais des variations sur certains points. Si je prends la classe SmartPtr de Loki par exemple, on peut avoir différentes instances (template) de celle-ci ayant la même politique d'Ownership (et donc des variations des autres politiques). Cependant c'est une question dont la réponse m'intéresse assez peu, au besoin il suffit d'introduire un Concept StrictOwnership et une fonction pourra accepter ce qu'on veut (en C++14 / C++17).
    Mon point est que plus la manière standard se répand, plus la justification d'une variation est difficile et qu'il arrive un moment où c'est préférable de faire avec la manière standard que de s'en écarter. Oh, il y en aura toujours qui s'écarteront. Certains échoueront, d'autres survivront dans une niche plus ou moins grande (+), quelques uns même feront changer la manière standard.

    [*]
    Cella rejoint le point de vue que j'exprimais plutôt avec white_tentacles, si tu considères les exceptions et que la résistance forte est de revenir à l'état avant l'évaluation de l'expression contenant ta fonction et que tu l'appliques, alors tu es systématiquement dans un cas conditionnel (*).

    (*) Sauf en situation noexcept, et j'ai tendance à supposer qu'on est pas en noexcept de manière général (et c'est toujours ce qu'on fait quand on parle d'écriture idiomatique et de bonnes habitudes en général).
    Une fonction qui accepte la responsabilité d'une ressource et en plus a un contact étroit? Elle me semble avoir trop de responsabilités. Tu as un exemple précis?

    (+) Qt est typique, il domine tellement sa niche qu'il en est quasiment incontournable dedans et peut se permettre d'ignorer ce qui se fait ailleurs.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  12. #72
    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
    Pour le premier point, ce que tu dis est justement ce qui fait que c'est pour moi une interrogation et pas encore une certitude.

    Pour le second point, non je n'ai pas d'exemple (si j'en trouve un entre temps, j'éditerais). Et je comprends bien ce que tu dis, mais j'envisageais plutôt les choses sous le point de vue : j'ai besoin de faire tel service (bien défini et qui peut potentiellement lancer) et pour ce faire j'ai besoin d’acquérir la responsabilité d'un ressource.

    Dans cette situation je vois mal ce qu'apporterait le fait d'avoir 2 services : un qui prend la responsabilité et un qui fait le boulot. Le premier service serait totalement artificiel.

    Edit exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    struct A {
      std::vector<std::unique_ptr<int>> v;
     
      void foo(std::unique_ptr<int>&& p)
      { v.push_back(std::move(p)); }
    };

  13. #73
    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 Flob90 Voir le message
    Pour le premier point, ce que tu dis est justement ce qui fait que c'est pour moi une interrogation et pas encore une certitude.
    Pour les besoins courant, il va se trouver des idiomes standards, et la pression pour les utiliser plutôt qu'une alternative sera forte.

    Edit exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    struct A {
      std::vector<std::unique_ptr<int>> v;
     
      void foo(std::unique_ptr<int>&& p)
      { v.push_back(std::move(p)); }
    };
    Bon exemple. Bon, c'est du forwarding (hors template), mais le bon exemple est std::vector::push_back. Mais les cas où on arrive à donner la garantie forte comme std::vector::push sont quand même plutôt rares hors de quelques classes techniques.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  14. #74
    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
    Oui, je suis clairement d'accord que pour les besoin courant, std::unique_ptr est parfait. J'essayais juste de voir un peu plus loin

    Pour la suite, oui, j'ai clairement pas dit qu'on arrive toujours à assurer une résistance forte, mais c'est clairement ce qu'on suppose quand on parle de bonne habitude et d'écriture idiomatique.

    Quel est l'argument pour de copy-and-swap ? La volonté d'avoir une résistance forte, je ne vois pas pourquoi on le considérerait quand on établit l'écriture idiomatique des opérateur d'affectation mais pas quand il s'agit de définir des écritures idiomatiques pour les passages de paramètres.

    Si l'écriture avec && et pas une copie présenterait de gros défaut, alors je comprendrais la volonté de vouloir utiliser des copies. Mais là ce n'est pas le cas. Le seul défaut, déjà évoqué avant, c'est qu'on est pas certain de ce que vaut le unique_ptr après la fonction, cependant, quel est la pertinence de la question ?

    Qu'on utilise && ou copie, on se retrouve à écrire :
    Si j'écris ca et que je fait autre chose qu'une opération d'affectation ou un reset derrière (*), c'est que je code avec les pieds Donc pour moi la question de savoir ce qu'il vaut ensuite n'a aucun pertinence, je dis que je n'en veut plus, je ne devrait pas me préoccuper de ce qu'il vaut. Et d'autre part, si la signature est && est que l'unique_ptr n'est pas vide ensuite, c'est l'auteur de la fonction qui a fait un mauvais choix pour son implémentation (il aurait du mettre const std::unique_ptr<>&).

    NB: Tout ceci restant mon avis personnel.

    (*) Or situation d'exception, puisque dans ce cas je m'attend (toujours en exception forte) à être dans le même état qu'avant cette ligne : c'est à dire quand l'objet était encore à moi.

  15. #75
    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
    L'objectif pour moi, c'est le nothrow, tout le reste n'est que compromis

    Assurer une résistance forte demande qu'il y ait très peu de mutations ou bien un mécanisme de transactions plus évolués que ce dont j'ai l'habitude. Les besoins de parallélisation feront peut-être que ça se généralisera (le non mutable ou le transactionnel), mais la conscience des exceptions n'a pas suffit.
    Les MP ne sont pas là pour les questions techniques, les forums sont là pour ça.

  16. #76
    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
    Oui, évidement que le nothrow c'est l'idéal. Mais tu sais aussi bien que moi que tu as forcément besoin d'un système d'exception (quelque soit son nom et quelque soit le langage), dès que tu as des "fonctionnalités" non-pure et que c'est clairement le cas dès qu'on va introduire des IO, de l'utilisation de mémoire, du réseau, whatever.

    Et donc à défaut de pouvoir dire au gens "ça peut toujours être nothrow alors débrouillez-vous", je préfère dire, "voila une écriture qui marchera dans toutes les situations".

  17. #77
    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 Jean-Marc.Bourguet Voir le message
    On n'est vraiment pas d'accord. Tout d'abord, on n'a jamais parlé de shared_ptr jusqu'à présent. Mais bon, si tu veux le traiter.
    Je ne fais que me re-citer :
    Mais le fait est que tu ne pourras pas te "limiter" à la seule modification de cette classe, car, autrement, tu vas te retrouver avec des références non comptées pour tes shared_ptr ou avec des classes et des fonctions qui, manque de pot, manipulent justement le pointeur qui fini sous la responsabilité de ton unique_ptr et auxquelles il te sera impossible de signaler que le pointeur qu'elles manipulent est invalidé.
    j'aurais peut être du les mettre dans l'autre sens (ben non, les gens se seraient de toutes manière focalisés sur le fait que je parlais de shared_ptr!!), mais si j'ai cité les shared_ptr ici, c'est parce que le problème est, quoi qu'il arrive, similaire : à partir du moment où un pointeur intelligent d'une ressource, il doit prendre cette responsabilité depuis le début, ou la récupérer de pointeurs identiques qui accepteront de la lui remettre, autrement ne sera que problèmes en pagaille!
    Reprenons les bases. Il y a différentes politiques de gestion de la mémoire. Certaines sont locales, elles ne demandent comme collaboration entre composants participants que l'accord sur la politique à mener et sur la définition l'interfaces, le moyen de la mettre en œuvre reste d'un choix purement local. C'est le cas de la responsabilité non partagée.
    La manière dont est géré le pointeur n'implique qu'un choix purement local:
    Qu'il le soit au travers d'un std::unique_ptr, d'un boost::unique_ptr ou d'un NIHuniquePtr n'a effectivement d'importance qu'au niveau de la classe qui manipule ce pointeur intelligent, je te l'accorde.
    D'autres sont globales, elles nécessitent non seulement l'accord sur la politique et l'interface, mais aussi sur le moyen de la mise en œuvre. C'est le cas de la responsabilité partagée.
    Mais, là, je ne suis pas d'accord du tout :

    que l'on te parle de shared_ptr ou de unique_ptr (quelle que soit leur origine),
    dés le moment où un pointeur intelligent, quel qu'il soit, rentre dans la danse, tu dois en tenir compte de manière globale.

    autrement, tu te retrouveras toujours soit avec une classe qui ne devrait pas se retrouver avec la responsabilité mais qui essaye de faire un delete sur ton pointeur, soit avec une classe qui continue à manipuler ton pointeur après que le pointeur intelligent ait fait son job.

    Dans les deux cas, ce sera l'erreur de segmentation garantie
    Dans le premier cas, un composant peut décider seul du moyen de mettre en œuvre sa politique, quel que soit son choix pour l'implémentation de la politique, il peut le modifier tant qu'il ne touche pas son interface sans que ça touche les autres. Et s'il touche son interface, ça ne va toucher ses partenaires que dans les alentours immédiats pour reprendre ta formulation. Et c'est vrai pour unique_ptr.
    Cela risque d'aller beaucoup plus loin que "les alentours immédiats".

    Cela peut toucher strictement n'importe quel classe ou n'importe quelle fonction qui manipule le pointeur en question.

    Si, sur les 100 classes dont je parlais là tantôt, tu en as 35 qui manipule un A*, 35 qui manipulent un B* et 30 qui manipulent un C*, si tu fais rentrer un pointeur intelligent (quel que soit son type ou son origine) dans la danse, c'est l'ensemble des 35 (ou des 30) classes qui seront impactées, parce que tu ne peut avoir aucune garantie qu'il n'y aura pas "un chemin indirect oublié de tous" (ou simplement inconnu du développeur, si l'une des classes est développée par "quelqu'un d'autre") qui mènera à l'utilisation du pointeur intelligent.
    Dans le second choix, une décision de changer le moyen de mettre en œuvre la politique va devoir toucher tout le monde, puisqu'il faut collaborer sur cette mise en œuvre ailleurs qu'aux interfaces.
    De manière générale, quel que soit le pointeur intelligent envisagé, même sans l'exposer outre mesure, le fait d'en introduire dans une logique qui ne l'utilisait pas avant aura un impacte généralisé, c'est ce que j'ai toujours dit et c'est ce que je répète encore ici
    C'est vrai aussi pour unique_ptr, qui était l'unique objet de la discussion jusqu'à présent.
    Bien sur que c'est aussi vrai pour unique_ptr, mais, que je sache, je ne l'ai pas exclu de mon raisonnement

    Tu as même cité toi meme le passage dans lequel, bien qu'incluant shared_ptr, je parlais de unique_ptr
    Et mon point est qu'unique_ptr est devenu le moyen standard de gérer la politique "possesseur unique", tout comme std::string est devenu le moyen standard de gérer les chaines de caractères, n'en déplaise à tout ceux qui, Qt en tête puisque tu le citais, gardent leur propre type de chaines pour de plus ou moins bonnes raisons, la compatibilité en est une bonne, l'attachement fétiche tenant de NIH en est une mauvaise.
    Sur ce point, je suis on ne peut plus d'accord avec toi
    Pour les chaines comme pour les pointeurs, quand j'ai de bonnes raisons de les manipuler autrement que de la manière standard, je le fais entre mes sous-composants, je ne l'expose pas dans mon interface. Parce que plus le temps va passer, tout comme il est devenu de plus en plus surprenant de voir des char const* ou des nihString dans les interfaces, de plus il va être surprenant de voir des T* ou des nihUniquePtr<T> dans celles-ci.
    Ca, je te l'accorde bien volontiers
    Les nihCountedPtr<T> vont eux rester plus longtemps, exactement pour la raison que tu donnes, c'est un choix non local et le basculement devra être fait d'un coup et pas par percolation à partir de noyaux précurseurs.
    Et je suis même d'accord avec toi sur ce point.

    Le point au final sur lequel on n'est pas d'accord, c'est sur le fait que même un std::unique_ptr (pour bien le mettre en opposition avec tous les autres de la même catégorie), ne doit pas s'exposer outre mesure.

    Pour moi, ce qu'il doit exposer, si tant est que cela ait sens au niveau des classes qui l'utilisent, ce sont les comportement qui permettent la gestion du pointeur dont il a la charge, et rien de plus.
    Tu n'as toujours pas montré en quoi unique_ptr ne convenait pas pour ça.
    S'il n'y a que ca...
    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
    void foo(Type * t){
        /* pas d'utilisation de std::unique_ptr */
    }
    void fctQuiUtiliseUniquePtr(Type * t){
       std::unique_ptr<Type> ptr(t)
    /* ce pourrait tout aussi bien être 
    void fctQuiUtiliseUniquePtr(std::unique_ptr<Type> ptr){ */
    /* manipulation diverses et variées */
    } ptr est détruit  ici -->delete t est invoqué, t est invalidé
    void bar(Type * t){
        /* ... */
        if(test){
            fctQuiUtiliseUniquePtr(t);
            /* ou
            fctQuiUtiliseUniquePtr(std::unique_ptr<Type>(t);
        } else{
            foo(t);
        }
        /* ici, qu'est ce qu'on a ??? */
        t->behaviour(); // une chance sur deux de planter
                              // une chance sur deux d'avoir un résultat totalement incohérent
    }
    Et ca, c'est sans compter toutes les fonctions et toutes les classes qui, d'une manière ou d'une autre, vont manipuler un Type * et qui risquent d'appeler bar, même si c'est de manière indirecte.

    Et ce risque est présent quel que soit le comportement de behaviour (j'aurais tout aussi bien pu écrire delete t à la place ).

    Et si ce n'est pas dans ton propre code que le problème survient, tu peux t'attendre à ce qu'il survienne dans le code utilisateur .
    Il dit deux choses:
    - return std::move(varlocale); est à éviter car return varlocale; est au moins aussi efficace. Personne n'a écrit le contraire. Je ne vois pas en quoi ça déconseille d'avoir unique_ptr<T> comme type de retour.
    Encore une fois, tu expose un détail d'implémentation, qui aura certes un comportement identique à ce que je préconise, qui, effectivement, se propagera normalement sur l'ensemble du projet, mais qui laisse à penser qu'il n'y a "qu'une certaine catégorie de fonctions" qui présentent ce comportement (sous entendu : c'est qu'il y a "une autre catégorie de fonction" qui ne prendra pas la responsabilité du pointeur).

    Au final, on en arrive, je te l'accorde, strictement au même point, à savoir que l'on arrive à avoir une approche globale du fait que les classes / fonction manipulent std::unique_ptr (mais exclusivement celui-là et pas un autre NihUnique_ptr qui ne faisait de mal à personne et qui aurait très bien pu rester à sa place, tant qu'il faisait ce qu'on attendait de lui (sans avoir la moindre nostalgie NIH ) ), si ce n'est:
    1. Que tu remplaces une règle générale ("il y a d'office quelque chose qui prend la responsabilité du pointeur, à vous de veiller à la récupérer (ou non) selon vos besoin") en un ensemble de règle particulières (à chaque fonction spécifique qui renvoit un unique_ptr)
    2. Que cela ne te dispense absolument pas de fournir l'un ou l'autre comportement associé à l'utilisation de unique_ptr (reset() get() )
    3. Que la règle générale est et restera donc "attention, chaque new doit être associé à un delete correspondant... sauf (la liste de toutes les fonctions)".

    Au final, tu te retrouves avec un règle de base vidée de sa substance ( à cause des exceptions qui couvre l'intégralité du scope de la règle de base ) et une exception par fonction... auxquelles il faut ajouter "l'exception à l'exception" pour chaque comportement similaire à get().

    Mais bons dieux! faisons simple, aussi bien pour la règle que pour l'interface!

    Les fonctions renvoient, quand c'est nécessaire, des pointeurs mais sont claires sur ce qu'elles font de la responsabilité, sans exposer le std::unique_ptr et la règle devient "la responsabilité d'un pointeur est, d'office, prise en charge par quelque chose, si vous voulez qu'il vous la donne, vous le lui demandez".

    Suis-je vraiment le seul à considérer que cette approche est beaucoup plus simple pour tout le monde
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  18. #78
    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 koala01 Voir le message
    S'il n'y a que ca...
    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
    void foo(Type * t){
        /* pas d'utilisation de std::unique_ptr */
    }
    void fctQuiUtiliseUniquePtr(Type * t){
       std::unique_ptr<Type> ptr(t)
    /* ce pourrait tout aussi bien être 
    void fctQuiUtiliseUniquePtr(std::unique_ptr<Type> ptr){ */
    /* manipulation diverses et variées */
    } ptr est détruit  ici -->delete t est invoqué, t est invalidé
    void bar(Type * t){
        /* ... */
        if(test){
            fctQuiUtiliseUniquePtr(t);
            /* ou
            fctQuiUtiliseUniquePtr(std::unique_ptr<Type>(t);
        } else{
            foo(t);
        }
        /* ici, qu'est ce qu'on a ??? */
        t->behaviour(); // une chance sur deux de planter
                              // une chance sur deux d'avoir un résultat totalement incohérent
    }
    J'ai beaucoup de mal avec ton exemple car je ne vois pas ce que tu cherches vraiment à montrer.

    La discussion à la base, tout du moins telle que je l'ai comprise, portait sur : si une classe (ou une fonction) doit acquérir la responsabilité exclusive faut-il passer une pointeur brut ou un unique_ptr.
    Or dans ton exemple tu compare une fonction qui n'acquiert pas la responsabilité (foo) avec une fonction qui l'acquiert (fctQuiUtiliseUniquePtr). Ca ne me semble pas être le sujet tout comme il ne me semble pas que quiconque ait suggéré de prendre systématiquement la responsabilité.

    La comparaison qui fait sens est plutôt celle que tu évoques très rapidement (comme un simple détail) entre void fctQuiUtiliseUniquePtr(Type * t) et
    void fctQuiUtiliseUniquePtr(std::unique_ptr<Type> ptr) dans le cas où la fonction doit prendre la responsabilité exclusive. Et à mon sens, l'utilisation de unique_ptr est préférable car elle indique clairement le transfert de responsabilité (et donc qu'il ne faut pas faire l'appel t->behaviour()).

  19. #79
    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 gl Voir le message
    J'ai beaucoup de mal avec ton exemple car je ne vois pas ce que tu cherches vraiment à montrer.

    La discussion à la base, tout du moins telle que je l'ai comprise, portait sur : si une classe (ou une fonction) doit acquérir la responsabilité exclusive faut-il passer une pointeur brut ou un unique_ptr.
    Or dans ton exemple tu compare une fonction qui n'acquiert pas la responsabilité (foo) avec une fonction qui l'acquiert (fctQuiUtiliseUniquePtr). Ca ne me semble pas être le sujet tout comme il ne me semble pas que quiconque ait suggéré de prendre systématiquement la responsabilité.
    Relis peut être les dernières interventions de jean marc et les miennes, et tu remarqueras sans doute que je justifie le fait que je ne suis pas du tout d'accord avec son argument du
    A nouveau tes arguments ressemblent furieusement à ceux que j'ai entendu il y a 15 ans contre std::string dans les interfaces. Il y en a d'ailleurs qui tiennent toujours ces raisonnements, mais leur nombre a bien décru.
    Tu comprendras peut etre aussi que j'essaye d'expliquer clairement que si les deux décisions sont parfaitement justifiables et justifiées, la première impliquera des changements très limités (à la classe modifiée et à quelques fonctions connexes) alors que la seconde impliquera des modifications d'ordre global.

    Tu remplaces le if else par autant d'appel de fonctions prenant un pointeur que tu veux, par l'appel, pourquoi pas, de comportement polymorphes, et tu te rend compte que "avec un peu de chance", tu ne passeras que par des fonctions qui se contenteront d'utiliser le pointeur, sans essayer d'en prendre la responsabilité, alors que "avec un peu de chance", tu vas très rapidement en arriver à essayer d'invoquer un comportement sur un pointeur invalidé ou un delete sur un pointeur qui aura déjà été détruit par décision du pointeur intelligent.

    Si tu n'as pas une vision globale de l'ensemble des chemins qui mènent à l'utilisation de fctQuiUtiliseUniquePtr, si tu ne prends pas l'ensemble de ces chemins en compte, tu te retrouveras fatalement avec un comportement indéfinis. La question n'est même plus de savoir si tu auras un comportement indéfini en ayant oublié un "chemin de traverse oublié de tous", la question sera de savoir quand cela arrivera.
    La comparaison qui fait sens est plutôt celle que tu évoques très rapidement (comme un simple détail) entre void fctQuiUtiliseUniquePtr(Type * t) et
    void fctQuiUtiliseUniquePtr(std::unique_ptr<Type> ptr) dans le cas où la fonction doit prendre la responsabilité exclusive. Et à mon sens, l'utilisation de unique_ptr est préférable car elle indique clairement le transfert de responsabilité (et donc qu'il ne faut pas faire l'appel t->behaviour()).
    Et cela, c'est la deuxième partie de mon argumentaire: fctQuiUtiliseUniquePtr devient une fonction qui a un comportement exceptionnel par rapport au reste.

    Le problème, c'est que la seule chose dont tu pourras jamais être sur, c'est que l'appelant direct de fctQuiUtiliseUniquePtr a bien compris qu'il transmettait la responsabilité du pointeur à cette fonction, et que tu ne pourras jamais garantir que l'une des fonctions qui, de manière (parfois très) indirecte, aura fait appel à fctQuiUtiliseUniquePtr aura bel et bien conscience que l'appel "anodin" d'une fonction qui n'a rien à voir avec fctQuiUtiliseUniquePtr aura comme résultat d'avoir retiré la responsabilité du pointeur, et très vraisemblablement de l'invalider.

    Tu peux tres bien avoir "quelque part dans le code" une fonction proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void bar( Type * t){
        /* fait plein de choses avant*/
        doSomething(t);/* fonction qui  appelle une fonction qui appelle  une fonction
                        * qui appelle ...fctQuiUtiliseUniquePtr 
                        */
        /* fait plein de choses après, sans toucher à t */
        t->behaviour();
        /* voir delete t */
    }
    Si tu n'as pas conscience que bar fait, de manière particulièrement indirecte, appel à fctQuiUtiliseUniquePtr, tu vas vraiment t'amuser pour arriver à déterminer d'où vient ton erreur de segmentation
    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. #80
    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 koala01 Voir le message
    Relis peut être les dernières interventions de jean marc et les miennes, et tu remarqueras sans doute que je justifie le fait que je ne suis pas du tout d'accord avec son argument du

    A nouveau tes arguments ressemblent furieusement à ceux que j'ai entendu il y a 15 ans contre std::string dans les interfaces. Il y en a d'ailleurs qui tiennent toujours ces raisonnements, mais leur nombre a bien décru.
    Je les ai lu et je ne vois toujours pas le rapport entre ta comparaison foo vs fctQuiUtiliseUniquePtr et la problèmatique.
    Pour la raison que foo n'ayant pas besoin de prendre l'ownership, elle n'est pas dans le contexte du problème. Et dans le cas d'une fonction devant prendre cet ownership (fctQuiUtiliseUniquePtr dans ton exemple), elle la prendra quoique tu utilises.

    En fait j'ai l'impression que tu cherches à montrer "qu'il ne faut pas utiliser unique_ptr dans l'interface car il faut éviter de prendre l'ownership dans une classe/fonction" et non "qu'il ne faut pas utiliser unique_ptr même lorsque la classe/fonction doit prendre l'ownership".
    Bref dans ton exemple tu pars du principe qu'il y a transfert de responsabilité car on utilise un unique_ptr alors que le point de Jean-Marc et d'autres porte sur le fait d'utiliser dans l'interface unique_ptr car il y a transfert de responsabilité.


    Citation Envoyé par koala01 Voir le message
    Tu remplaces le if else par autant d'appel de fonctions prenant un pointeur que tu veux, par l'appel, pourquoi pas, de comportement polymorphes, et tu te rend compte que "avec un peu de chance", tu ne passeras que par des fonctions qui se contenteront d'utiliser le pointeur, sans essayer d'en prendre la responsabilité, alors que "avec un peu de chance", tu vas très rapidement en arriver à essayer d'invoquer un comportement sur un pointeur invalidé ou un delete sur un pointeur qui aura déjà été détruit par décision du pointeur intelligent.

    Si tu n'as pas une vision globale de l'ensemble des chemins qui mènent à l'utilisation de fctQuiUtiliseUniquePtr, si tu ne prends pas l'ensemble de ces chemins en compte, tu te retrouveras fatalement avec un comportement indéfinis. La question n'est même plus de savoir si tu auras un comportement indéfini en ayant oublié un "chemin de traverse oublié de tous", la question sera de savoir quand cela arrivera.

    Et cela, c'est la deuxième partie de mon argumentaire: fctQuiUtiliseUniquePtr devient une fonction qui a un comportement exceptionnel par rapport au reste.

    Le problème, c'est que la seule chose dont tu pourras jamais être sur, c'est que l'appelant direct de fctQuiUtiliseUniquePtr a bien compris qu'il transmettait la responsabilité du pointeur à cette fonction, et que tu ne pourras jamais garantir que l'une des fonctions qui, de manière (parfois très) indirecte, aura fait appel à fctQuiUtiliseUniquePtr aura bel et bien conscience que l'appel "anodin" d'une fonction qui n'a rien à voir avec fctQuiUtiliseUniquePtr aura comme résultat d'avoir retiré la responsabilité du pointeur, et très vraisemblablement de l'invalider.
    Le contrat de ta fonction fctQuiUtiliseUniquePtr dit qu'elle prends l'ownership. C'est aux appelants direct de s'assurer qu'ils peuvent le lui transmettre, que ce soit en le prenant eux même vis à vis de la couche au dessus (et en le faisant apparaître dans leur contrat), en copiant avant de transférer la copie, en le récupérant ensuite, etc.

    Citation Envoyé par koala01 Voir le message
    Tu peux tres bien avoir "quelque part dans le code" une fonction proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void bar( Type * t){
        /* fait plein de choses avant*/
        doSomething(t);/* fonction qui  appelle une fonction qui appelle  une fonction
                        * qui appelle ...fctQuiUtiliseUniquePtr 
                        */
        /* fait plein de choses après, sans toucher à t */
        t->behaviour();
        /* voir delete t */
    }
    Si tu n'as pas conscience que bar fait, de manière particulièrement indirecte, appel à fctQuiUtiliseUniquePtr, tu vas vraiment t'amuser pour arriver à déterminer d'où vient ton erreur de segmentation
    Le problème n'est pas tant que fctQuiUtiliseUniquePtr utilise un unique_ptr dans son interface mais que dans la chaîne d'appel une fonction ne fait pas correctement son travail de respect des contrats.

    D'autant, que l'on parlait plutôt dans cette discussion, de l'usage de unique_ptr dans l'interface lorsqu'une classe/fonction doit prendre l'ownership.
    Donc que tu utilises ou non unique_ptr, il y a transfert de responsabilité et donc le souci mentionné ici.

    L'utilisant dans l'interface d'un unique_ptr a au moins le mérite de documenter le contrat (sans compter qu'il doit alors être possible d'outiller pour détecter les cas d'utilisation après transfert de responsabilité).

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