Forum Programmation.c++ Petit soucis avec les shared_ptr

Posté par . Licence CC by-sa
Tags :
0
3
juin
2016

Bonjour,

Je cherche à faire un petit code en remplaçant les pointeurs par des shared_ptr. J’ai probablement un défaut de design induit par mon expérience des pointeurs historiques… Voici un bout de code qui met en avant mon soucis :

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

class MyInt {
public:
  static void init(int n);
  static MyInt &get(int n);

  int get_id() {return id;}
protected:
  MyInt(int n):id(n){}

private:
  int id;
  static vector<MyInt> MyInts;
};

vector<MyInt> MyInt::MyInts;

void MyInt::init(int n)
{
  MyInts.reserve(n);
  for (int i=0;i<n;i++)
    MyInts.push_back(MyInt(i));
}

MyInt & MyInt::get(int n)
{
  return MyInts[n];
}

int main ()
{
  int N;
  cout << "Nb de MyInt : ";
  cin >> N;

  N = max(4,N); /* Pour l'exemple */

  MyInt::init(N);

  // Pernons un share pointer sur l'element 3.
  auto p1 = make_shared<MyInt>(MyInt::get(3));

  auto p2 = make_shared<MyInt>(MyInt::get(3));

  cout << "p1 : id = " << p1->get_id() << ", n_ptr = " << p1.use_count() << ", addr = " << p1.get() << endl;
  cout << "p2 : id = " << p2->get_id() << ", n_ptr = " << p2.use_count() << ", addr = " << p2.get() <<  endl;
}

Voici ce que donne l’exécution.

Nb de MyInt : 5
p1 : id = 3, n_ptr = 1, addr = 0x87a04c
p2 : id = 3, n_ptr = 1, addr = 0x87a07c

Le résultat est que mes deux pointeurs n’ont pas la même valeur, là où je voudrais qu’ils partagent le compteur de référence. Je n’avais pas très envie de mettre un vector<shared_ptr<MyInt>> en type. C’est peut-être obligatoire…

Sinon, j’imaginais une sorte de get_shared qui irait chercher si un shared_ptr pointe déjà sur l’objet ou s’il faut un créer un nouveau.

À votre avis ?

  • # Mon avis mais je ne connais rien au C++

    Posté par . Évalué à 1.

    Ca va peut être faire avancer le schimlblick, ou pas…
    mais il me semble que la fonction MyInt::get ne retourne pas ce qu'elle devrait
    ->return MyInts[n];
    Moi j'imagine qu'il faut mettre
    ->return &MyInts[n];
    pour que cela fonctionne
    mais j'ai peut être pas compris ce que tu voulais faire.

  • # Constructeur de copie

    Posté par (page perso) . Évalué à 2.

    Attention, make_shared ne créé pas un pointeur à partir d'un objet, il instancie un nouvel objet et retourne un shared_pointer dessus ! Tu as le constructeur de copie qui est implicitement appelé.

    Ainsi, dans p1 tu réalises une copie de l'instance de MyInt retournée par MyInt::get(3). Il en est de même pour p2. C'est donc normal que les deux instances ne correspondent pas.

    Je pense qu'il faudrait plus que tu écrives un truc du genre :

    auto p1 = shared_pointer<MyInt>(&MyInt::get(3));
    auto p2 = shared_pointer<MyInt>(&MyInt::get(3));

    Maintenant, le compteur ne sera pas partagé (mais p1.get et p2.get devraient retourner la même valeur).

    Je n’avais pas très envie de mettre un vector>

    Je pense que c'est ce que tu devrais faire. Sans cela, si tu fais un shared_pointer et qu'il n'est plus référencé nul part, à sa destruction il va libérer la mémoire. Ce qui n'est certainement pas ce qui est souhaité car ton tableau contiendrait des instances dont le destructeur aura été appelé.

    • [^] # Re: Constructeur de copie

      Posté par . Évalué à 2.

      Rajouter le & devant ne résout pas le problème car ça ne compile pas.

      J’ai le choix entre mettre les pointeurs historiques, ou faire un my_ptr<MyInt> qui me garanti juste qu’on pointe sur quelque chose de valide.

      J’avais pensé mettre un shared_ptr à la place du vector, mais je ne pense pas qu’on puisse ensuite pointer sur les éléments individuellement, ni y accéder via l’operator [].

      • [^] # Re: Constructeur de copie

        Posté par . Évalué à 2.

        L’operateur [] sera pour la norme 2017.

      • [^] # Re: Constructeur de copie

        Posté par (page perso) . Évalué à 2.

        Rajouter le & devant ne résout pas le problème car ça ne compile pas.

        Si tu as fait un copier/coller, c'est parce que j'ai mis "shared_pointer" au lieu de "shared_ptr". Sinon, ça compile, je viens d'essayer.

        • [^] # Re: Constructeur de copie

          Posté par . Évalué à 2.

          Ben avec gcc et clang ça ne marche pas chez moi.

          In file included from main.cpp:1:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/iostream:39:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/ostream:38:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/ios:42:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/ios_base.h:41:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/locale_classes.h:40:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/string:41:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/allocator.h:46:
          In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/x86_64-pc-linux-gnu/bits/c++allocator.h:33:
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/ext/new_allocator.h:120:23: error: no matching constructor for initialization of 'MyInt'
                  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
                                       ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/alloc_traits.h:253:8: note: in instantiation of function template specialization '__gnu_cxx::new_allocator<MyInt>::construct<MyInt, MyInt *>' requested here
                  { __a.construct(__p, std::forward<_Args>(__args)...); }
                        ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/alloc_traits.h:399:4: note: in instantiation of function template specialization 'std::allocator_traits<std::allocator<MyInt> >::_S_construct<MyInt, MyInt *>' requested here
                  { _S_construct(__a, __p, std::forward<_Args>(__args)...); }
                    ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/shared_ptr_base.h:515:30: note: in instantiation of function template specialization 'std::allocator_traits<std::allocator<MyInt> >::construct<MyInt, MyInt *>' requested here
                    allocator_traits<_Alloc>::construct(__a, _M_ptr(),
                                              ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/ext/new_allocator.h:120:23: note: in instantiation of function template specialization 'std::_Sp_counted_ptr_inplace<MyInt, std::allocator<MyInt>, 2>::_Sp_counted_ptr_inplace<MyInt *>' requested here
                  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
                                       ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/alloc_traits.h:253:8: note: in instantiation of function template specialization '__gnu_cxx::new_allocator<std::_Sp_counted_ptr_inplace<MyInt, std::allocator<MyInt>, 2>
                >::construct<std::_Sp_counted_ptr_inplace<MyInt, std::allocator<MyInt>, 2>, const std::allocator<MyInt>, MyInt *>' requested here
                  { __a.construct(__p, std::forward<_Args>(__args)...); }
                        ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/alloc_traits.h:399:4: note: (skipping 2 contexts in backtrace; use -ftemplate-backtrace-limit=0 to see all)
                  { _S_construct(__a, __p, std::forward<_Args>(__args)...); }
                    ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/shared_ptr_base.h:1089:14: note: in instantiation of function template specialization 'std::__shared_count<2>::__shared_count<MyInt, std::allocator<MyInt>, MyInt *>' requested here
                  : _M_ptr(), _M_refcount(__tag, (_Tp*)0, __a,
                              ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/shared_ptr.h:316:4: note: in instantiation of function template specialization 'std::__shared_ptr<MyInt, 2>::__shared_ptr<std::allocator<MyInt>, MyInt *>' requested here
                  : __shared_ptr<_Tp>(__tag, __a, std::forward<_Args>(__args)...)
                    ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/shared_ptr.h:587:14: note: in instantiation of function template specialization 'std::shared_ptr<MyInt>::shared_ptr<std::allocator<MyInt>, MyInt *>' requested here
                return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,
                       ^
          /usr/lib/gcc/x86_64-pc-linux-gnu/4.9.3/include/g++-v4/bits/shared_ptr.h:603:19: note: in instantiation of function template specialization 'std::allocate_shared<MyInt, std::allocator<MyInt>, MyInt *>' requested here
                return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
                            ^
          main.cpp:91:13: note: in instantiation of function template specialization 'std::make_shared<MyInt, MyInt *>' requested here
            auto p1 = make_shared<MyInt>(&MyInt::get(3));
                      ^
          main.cpp:13:7: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'MyInt *' to 'const MyInt' for 1st argument; dereference the argument with *
          class MyInt {
                ^
          main.cpp:13:7: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'MyInt *' to 'MyInt' for 1st argument; dereference the argument with *
          class MyInt {
                ^
          main.cpp:27:3: note: candidate constructor not viable: no known conversion from 'MyInt *' to 'int' for 1st argument
            MyInt(int n):id(n){}
            ^
          1 error generated.
          
  • # peut-être ceci ?

    Posté par . Évalué à 1.

    auto p1 = make_shared<MyInt>(MyInt::get(3));
    auto p2 = p1

    ça rend ça :

    Nb de MyInt : 5
    p1 : id = 3, n_ptr = 2, addr = 0x93e3030
    p2 : id = 3, n_ptr = 2, addr = 0x93e3030
    

    Comme il a été dit dans les commentaires précédents, make_shared va construire une copie de Myint::get(3), et initialiser un shared_ptr sur cette copie, pas sur le troisème élément de ton tableau.

    • [^] # Re: peut-être ceci ?

      Posté par . Évalué à 1. Dernière modification le 03/06/16 à 17:58.

      aussi, tu peux t'assurer facilement du fonctionnement de la mécanique de comptage en mettant p2 dans un scope :

        auto p1 = make_shared<MyInt>(MyInt::get(3));
      
        cout << "p1 : id = " << p1->get_id() << ", n_ptr = " << p1.use_count() << ", addr = " << p1.get() << endl;
        {
            auto p2 = p1;
      
            cout << "p1 : id = " << p1->get_id() << ", n_ptr = " << p1.use_count() << ", addr = " << p1.get() << endl;
            cout << "p2 : id = " << p2->get_id() << ", n_ptr = " << p2.use_count() << ", addr = " << p2.get() <<  endl;
        }
        cout << "p1 : id = " << p1->get_id() << ", n_ptr = " << p1.use_count() << ", addr = " << p1.get() << endl;

      résultat :

      Nb de MyInt : 5
      p1 : id = 3, n_ptr = 1, addr = 0x8289030
      p1 : id = 3, n_ptr = 2, addr = 0x8289030
      p2 : id = 3, n_ptr = 2, addr = 0x8289030
      p1 : id = 3, n_ptr = 1, addr = 0x8289030

    • [^] # Re: peut-être ceci ?

      Posté par . Évalué à 2.

      Oui, je sais que ça marche comme ça. Mais dans mon vrai code, qui est une implémentation de graph, je fais les get dans les constructeur de liens entre mes Nodes… pas à deux lignes d’intervalle comme dans l’exemple.

      Bon, en gros, je continue avec des pointeurs standard… je devrais me trouver un autre exercice pour utiliser les pointeurs intelligent. Moi qui pensait qu’on allait pouvoir s’affranchir complètement des pointeurs historique…

      • [^] # Re: peut-être ceci ?

        Posté par . Évalué à 1. Dernière modification le 03/06/16 à 21:34.

        C'est normal, vouloir faire un shared_ptr à partir d'un pointeur normal, c'est de la bidouille.
        ```Il faut modifier la classe pour que      vector<MyInt> devienne     vector<shared_ptr<MyInt>>.
        
        
        
        • [^] # Re: peut-être ceci ?

          Posté par . Évalué à 2.

          C’est quoi la bonne méthode pour pointer sur un objet static ou global en c++11 ?

          • Garder les pointeurs standard ?
          • Les références dans mon cas je peux le faire.
          • Les shared ptr ne semble pas adapté.
          • Les weak ptr peut-être ?

          Un avis sur la question ?

          • [^] # Re: peut-être ceci ?

            Posté par . Évalué à 2.

            Mec, le but des shared_ptr, unique_ptr et autres est d'implanter une notion de propriété (ownership) qui, en gros représente la responsabilité de détruire l'objet et libérer la mémoire sur laquelle il a été construit.
            Si l'objet est global ou qu'il est alloué sur la pile, il est détruit automatiquement quand le programme s’arrête ou quand le programme quitte le scope dans lequel il a été déclaré. En conséquence, il n'y a aucun intérêt à créer un pointeur intelligent dessus.

            Si tes objets sont placés dans un graphe, cette responsabilité devrait revenir soit au graphe lui même, soit à des objets intermédiaires, par exemple les vector que tu utilises dans ton implantation. Su tu utilises un <vector<shared_ptr<T>>, les shared_ptr auront cette responsabilité, avec un effet de bord : tu pourras alors avoir détruit ton objet graphe et toujours avoir des T non-détruits si jamais tu as gardé des copies de shared_ptr. À toi de décider si c'est une bonne ou mauvaise chose .

            • [^] # Re: peut-être ceci ?

              Posté par . Évalué à 2.

              Je confirme, les pointeurs intelligents ne permettent pas de s'affranchir du minimum de réflexion nécessaire avant la construction d'un objet, que ce soit new/make_shared/make_unique

              S'affranchir des new & delete, oui, s'affranchir des pointeurs historique et ne balader que des pointeurs dit intelligents, j'aime moins. les shared_ptr incluent nécessairement un overhead pour chaque accès, et il pousse les gens à s'affranchir de la question "Qui possède le pointeur?",

              j'ajouterai que je vois rarement un shared_ptr y compris lorsque l'on est pas sensé modifier l'objet (en même temps c'est galère à gérer. Normalement on a plus trop à balader des pointeur, et ce depuis longtemps, on balade des ref, des const ref, et si on se paye un pointeur, c'est qu'il peut être NULL (enfin de mon point de vue)

              Il ne faut pas décorner les boeufs avant d'avoir semé le vent

          • [^] # Re: peut-être ceci ?

            Posté par (page perso) . Évalué à 1.

            Dans un graphe, les nœuds et les liens pourraient être alloués dynamiquement. Là, tu pourrais utiliser les std::shared_ptr . Est-ce toi qui implémente le graphe ?

            • [^] # Re: peut-être ceci ?

              Posté par . Évalué à 2.

              Oui, mais j’ai fait simple. Comme j’ai un nombre de nœuds et de liens connu au démarrage (lecture fichier) et que c’est statique ensuite, j’ai fait un tableau vecteur de nœuds, ensuite je créé également un vecteur de liens. Je peux construire mes liens avec des références, puisque connue à la construction et que le reserve me garanti que les éléments de mon vecteur ne seront pas déplacé.

              Ce que j’ai fait n’a rien de complexe à la base, je voulais m’entrainer au c++11 pour une fois que je ne suis pas coincé par du 03 voir antérieur… du coup, c’est un fail pour mon essai des shared_ptr. Par contre j’ai progressé dans le choix de quand les utiliser ou non.

              La solution de faire des vecteurs de pointeurs me semble faire de l’overengineering… donc pas de c++11 pour cette partie :'(

          • [^] # Re: peut-être ceci ?

            Posté par (page perso) . Évalué à 2.

            Pour l'ensemble des lecteurs, j'aimerais quand même résumer cela car c'est fondamentale.

            Une variable globale est connue du programmeur et il "souhaitera" qu'elle soit "instanciée" avant-même le début de la fonction principale (main) et "détruite" après la sortie de cette fonction. Le programmeur ne devra normalement pas ce soucier de cela.

            Une variable locale se situe dans un bloque d'instructions délimité par des accolades. La variable est instanciée à l'endroit-même de sa déclaration et détruite après la fin du bloque d'instructions, quoi qu'il en soit ! même si une exception est levée par exemple, ou un goto (à éviter par ailleurs) effectué… C'est ça l'idée, même s'il y a de rares exceptions… Là encore, le programmeur n'a pas à se faire de soucis…

            Par contre une variable dynamique est instanciée suite à l'instruction new et détruite avec l'instruction delete. C'est au programmeur de ce soucier qu'à chaque new correspondra un delete opportun. Car cette variable subsistera à tout le reste, que ce soit la fin d'un bloque d'instructions, sortie de fonction, etc. Même la fin du programme, de la fonction principale ne garanti rien. L'adresse allouée de cette variable sera donc précieuse entre le new et le delete, car c'est sur base de cette adresse que le delete pourra se faire.

            C'est là qu'intervienne les pointeurs intelligents, encapsuler la "responsabilité" d'une variable dynamique à l'intérieur d'une autre variable, qui par exemple, sera locale. Ainsi, même suite à la levée d'une exception, le pointeur intelligent (selon l'exemple d'une variable locale) sera détruit, et par-là-même un delete effectué sur la variable dynamique.

            Quant à la transmission d'un pointeur à une fonction. Cela signifie généralement que, contrairement à une référence (principalement par convention), l'adresse à pu être stockée dans une variable subsistante à la sortie de la fonction. Ce n'est pas une obligation mais il faudra se poser la question… L'autre possibilité est que cette adresse soit utilisée comme littérateur ou comme base d'une série (array[]). On se souciera alors de savoir comment se fait le parcours et sur quelle base elle prendra fin (nombre d'éléments, adresse de fin, etc. ?). L'adresse d'une variable globale ou locale s'obtient avec l'opérateur & alors qu'un pointeur intelligent prévoira généralement une fonction membre (le fameux std::shared_ptr::get par exemple).

            "Par convention" car il est possible à l'intérieur d'une fonction de récupérer l'adresse d'un variable argument de type référence et de la stocker. Mais à mon habitude, je ne m'y attendrais pas… C'est pas "intuitif" me semble-t-il…

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.