Forum Programmation.c++ classe C++, membre structure C et allocation dynamique

Posté par  (site web personnel, Mastodon) . Licence CC By‑SA.
Étiquettes :
3
21
avr.
2017

Bonjour,
je m'essaye (débutant) à développer une classe c++ dont certains membre sont des pointeurs vers des structures définies dans une bibliothèque c.
Par exemple :

/* maclasse.hh */
#include <une_lib_en_c.h>

class MaClasse {
public:
  MaClasse();
  ~MaClasse();
  int init();
private:
  ma_struct_c *s; //struct définie dans une_lib_en_c.h
};

MaClasse::MaClasse() {
  s = nullptr;
}

MaClasse::~MaClasse() {
  if (s != nullptr) {
    // s_destroy définie dans une_lib_en_c.h    
    s_destroy(&s);
    delete s;
  }
}

// Comme il semble que ce n'est pas possible/propre de vérifier
// la bonne exécution de l'allocation dynamique de s dans le constructeur,
// fonction d'initialisation avec éventuelle erreur en retour.
int MaClasse::init() {
  s = new ma_struct_c;
  if (s != nullptr) {
    // s_init définie dans une_lib_en_c.h
    // retourne 0 en cas de succès.
    return s_init(s);
  }
  else {
    return 1;
  }
}

Quelques questions :

Est-ce que c'est propre/utile/important de remplacer les malloc/free par des new/delete ?

Est-ce que cette implémentation est correcte pour la bonne libération de la mémoire allouée dynamiquement dans MaClasse::init() lorsque le destructeur de MaClasse est appelé ?

Bonus : Puisque j'ai au moins un pointeur comme variable membre, je suppose que je dois faire gaffe à redéfinir le constructeur de copie de MaClasse ?

Merci de vos conseils ou suggestions.

  • # quelques éléments de réponse

    Posté par  . Évalué à 4. Dernière modification le 21 avril 2017 à 17:15.

    • En ce qui concerne new VS malloc pour allouer une struct C, je n'ai pas vraiment d'avis ; tant que tu les utilises correctement, ça na va pas changer l'univers. new peut utiliser un autre allocateur que malloc mais tant que tu ne mélanges pas malloc/free avec new/delete sur le même objet, ça ira.
    • La spécification C++ dit que si new échoue à allouer de la mémoire, il émet une exception de type std::bad_alloc, du coup, il est inutile de tester le pointeur de retour envers nullptr.
    • Personnellement, je décourage l'utilisation d'une fonction public du genre init(). Ça laisse plus d'opportunités à l'utilisateur de la classe de se tirer une balle dans le pied. Je suis partisan d'insérer le code d'init dans le constructeur et de balancer une exception en cas d'erreur. Au moins l'utilisateur ne se retrouvera jamais avec un objet inutilisable.
    • si tu as testé le pointeur à la construction, le tester à la destruction est excessif.
    • tu peux probablement déléguer la gestion de ton pointeur à un pointeur intelligent tel que unique_ptr, en définissant le Deleter idoine.
    • [^] # Re: quelques éléments de réponse

      Posté par  (site web personnel, Mastodon) . Évalué à 2.

      Merci de la réponse.

      Personnellement, je décourage l'utilisation d'une fonction public du genre init(). Ça laisse plus d'opportunités à l'utilisateur de la classe de se tirer une balle dans le pied. Je suis partisan d'insérer le code d'init dans le constructeur et de balancer une exception en cas d'erreur. Au moins l'utilisateur ne se retrouvera jamais avec un objet inutilisable.

      Ben, c'était mon plan de départ, mais j'ai cru comprendre (peut-être par erreur) que le lancement d'une exception dans un constructeur laisse la construction de l'objet inachevée, et que donc son destructeur n'est pas appelé au moment opportun. Si j'avais déjà alloué des trucs au moment du lancement de l'exception, il faut que je fasse le ménage avant de lancer l'exception ?

      tu peux probablement déléguer la gestion de ton pointeur à un pointeur intelligent tel que unique_ptr, en définissant le Deleter idoine.

      Cela m'intéresse aussi, mais, hum, ça sera pour la deuxième semaine. Comment utiliser un unique_ptr si je dois passer ce pointeur à une fonction C qui le modifie (ou qui modifie la valeur de certains membres de la structure pointée) ?

      • [^] # Re: quelques éléments de réponse

        Posté par  . Évalué à 3. Dernière modification le 22 avril 2017 à 00:47.

        Ben, c'était mon plan de départ, mais j'ai cru comprendre (peut-être par erreur) que le lancement d'une exception dans un constructeur laisse la construction de l'objet inachevée, et que donc son destructeur n'est pas appelé au moment opportun. Si j'avais déjà alloué des trucs au moment du lancement de l'exception, il faut que je fasse le ménage avant de lancer l'exception ?

        C'est une question pertinente. Ce que tu décris est juste, mais une nuance t'échappe. Effectivement, si le constructeur d'un objet émet une exception, l'objet n'est pas considéré comme construit et son destructeur n'est pas appelé. Toutefois, le destructeur des membres et le destructeur des classes de base sont appelés.
        Ce qui m'amène à une réponse plus complète : effectivement, si une exception est lancée depuis le constructeur au milieu de l'allocation de différentes ressources, les libérer proprement risque d'être particulièrement ardu. En conséquence, il est fortement déconseillé pour une classe C++ de gérer directement plus d'une seule ressource. Les autres ressources doivent être gérées par des membres ou des classes de base, dont le destructeur (et donc la libération de la ressource) sera appelé automatiquement par le langage, même dans le cas où l'exception est lancée dans le constructeur.

      • [^] # Re: quelques éléments de réponse

        Posté par  . Évalué à 2.

        Cela m'intéresse aussi, mais, hum, ça sera pour la deuxième semaine. Comment utiliser un unique_ptr si je dois passer ce pointeur à une fonction C qui le modifie (ou qui modifie la valeur de certains membres de la structure pointée) ?

        Tu peux utiliser unique_ptr::get :

        #include <memory>
        #include <iostream>
        
        struct Blah
        {
            int value;
        };
        
        int main()
        {
           std::unique_ptr<Blah> unique{new Blah()};//zero-initialization
           std::cout << unique->value << '\n';//0
           Blah * ptr = unique.get();
           ptr->value = 42;
           std::cout << unique->value << '\n';//42
        }
        • [^] # Re: quelques éléments de réponse

          Posté par  (site web personnel, Mastodon) . Évalué à 1.

          Donc par exemple :

          /* maclasse.hh */
          #include <memory>
          #include "une_lib_en_c.h"
          
          class MaClasse {
          public:
            MaClasse();
            ~MaClasse();
          private:
            std::unique_ptr<ma_struct_c> s; //struct définie dans une_lib_en_c.h
          };
          
          MaClasse::MaClasse() {
            s = std::unique_ptr<ma_struct_c>(new ma_struct_c);
            // il faut que j'implémente un deleter ?
          }
          
          MaClasse::~MaClasse() {
          }

          et

          /* main.cpp */
          #include <exception>
          #include "MaClasse.hh"
          
          int main() {
            try {
              MaClasse::MaClasse m;
              // allocation s'est bien passée, je peux continuer ?
            }
            catch (std::exception const& e) {
              // ici j'attrape l'éventuelle std::bad_alloc ?
              // et je gère le fait que l'allocation s'est mal passée ?
            }
            return 0;
          }

          Dans quel cas faut-il implémenter un deleter pour un unique_ptr ? Notamment si je dois nettoyer la structure pointée avant qu'elle soit libérée ? Le deleterpar défaut est appelé par le destructeur de MaClasse ?
          Merci.

          • [^] # Re: quelques éléments de réponse

            Posté par  . Évalué à 5.

            Dans quel cas faut-il implémenter un deleter pour un unique_ptr ? Notamment si je dois nettoyer la structure pointée avant qu'elle soit libérée ? Le deleter par défaut est appelé par le destructeur de MaClasse ?

            Le deleter par défaut est appelé par le destructeur de unique_ptr après que le destructeur de ta classe a fini de s'exécuter (cf ma première réponse). En l’occurrence, tu pourrais le remplacer par un Deleter qui appelle la fonction de nettoyage C avant de libérer la mémoire :

            #include <memory>
            #include <iostream>
            #include <exception>
            
            extern "C"
            {
            struct my_c_struct {int data;};
            int s_init(my_c_struct * c){ std::cout << "C initializing\n"; return 1;}
            void s_destroy(my_c_struct *c){ std::cout << "C clean up\n";}
            }
            
            class MyClass
            {
            public:
              MyClass();
            private:
              struct Deleter
              {
                  void operator()(my_c_struct * p)
                  {
                      s_destroy(p);
                      delete p;
                  }
              };
              std::unique_ptr<my_c_struct, Deleter> s; 
            };
            
            MyClass::MyClass()
            : s(new my_c_struct)
            {
                int rc = s_init(s.get());
                if (rc != 0)
                {
                    throw std::runtime_error("could not initialize c_struct, rc = " + std::to_string(rc));
                }
            }
            
            int main()
            {
                try
                {
                    MyClass m;
                    // allocation s'est bien passée, je peux continuer ?
                }
                catch (std::exception const& e)
                {
                    std::cout << "runtime error: " << e.what() << '\n';
                    return 42;
                }
                return 0;
            }

            C'est une approche un peu bâtarde à mon goût parce que les responsabilités d'initialisation et de de nettoyage C ne sont pas du ressort du même objet : l'initialisation est faite dans le constructeur de MyClass et le nettoyage dans le Deleter du unique_ptr. Du point de vue ingénierie, ce n'est pas particulièrement idéal, d'ailleurs tu noteras que le code de nettoyage C est lancé même si l'initialisation échoue. Ça dépend probablement de ton API C, mais en général, c'est inutile, voire dangereux. Pour le coup, il serait peut-être préférable de laisser la responsabilité de gérer la mémoire au unique_ptr et de laisser à ta classe de gérer l'initialisation / nettoyage :

            #include <memory>
            #include <iostream>
            #include <exception>
            
            extern "C"
            {
            struct my_c_struct {int data;};
            int s_init(my_c_struct * c){ std::cout << "C initializing\n"; return 1;}
            void s_destroy(my_c_struct *c){ std::cout << "C clean up\n";}
            }
            
            class MyClass
            {
            public:
              MyClass();
              ~MyClass();
            private:
              std::unique_ptr<my_c_struct> s; 
            };
            
            MyClass::MyClass()
            : s(new my_c_struct)
            {
                int rc = s_init(s.get());
                if (rc != 0)
                {
                    throw std::runtime_error("could not initialize c_struct, rc = " + std::to_string(rc));
                }
            }
            
            MyClass::~MyClass()
            {
                s_destroy(s.get());
            }
            
            int main()
            {
                try
                {
                    MyClass m;
                    // allocation s'est bien passée, je peux continuer ?
                }
                catch (std::exception const& e)
                {
                    std::cout << "runtime error: " << e.what() << '\n';
                    return 42;
                }
                return 0;
            }

            Ici, le code de nettoyage C n'est appelé que si l'initialisation C a abouti.

            / et je gère le fait que l'allocation s'est mal passée ?

            Le catch va récupérer toutes les erreurs, notamment les erreurs d'allocation, et l'exception due à une mauvaise initialisation en C. Mais au moins toutes les ressources auront déjà été libérées au moment du catch, en gros, à ce niveau tu n'as plus qu'à logguer, tout l reste est déjà « géré ». Le fait de confier la libération des ressources aux destructeurs des objets te permet d'avoir une logique try - catch relativement simple comparé à d'autres langages.

Suivre le flux des commentaires

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