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

    Posté par . En réponse au message classe C++, membre structure C et allocation dynamique. É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.