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

C++ Discussion :

RAII et exceptions


Sujet :

C++

  1. #1
    Membre Expert Avatar de yildiz-online
    Homme Profil pro
    Architecte de domaine
    Inscrit en
    Octobre 2011
    Messages
    1 447
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Architecte de domaine

    Informations forums :
    Inscription : Octobre 2011
    Messages : 1 447
    Par défaut RAII et exceptions
    Bonjour a tous,

    J'ai un cas de figure un peu particulier, et étant débutant en C++, j'aimerais votre avis sur la marche a suivre.

    Le contexte d'exécution:

    App java -> charge dynamic lib A (en C++) -> charge dynamic lib B (en C).

    App java est une application qui est 'self recoverable' elle gère donc les erreurs de manière gracieuse et peut recharger les modules qui échouent en garantissant un fonctionnement continu, dans les limites du possible et raisonnable bien sur.

    Elle s'appuie sur lib A pour certaines opérations, lib A dont donc garantir la même robustesse et est chargée en continu(java ne permet pas de décharger une lib dynamique).
    Lib A doit charger dynamiquement(dlopen et consorts) une lib B pour certaines opérations, et la décharger au terme de ces opérations, il y a plusieurs implémentations différentes de lib B qu'il est possible de charger suivant le besoin courant.

    Lib A doit donc pour chaque chargement de lib B allouer des ressources propre a cette implémentation spécifique et les relâcher au terme de l'utilisation afin de garantir un contexte sain pour le prochain chargement et bien sur éviter les fuites mémoires.

    Tout ca pour en venir aux contraintes:
    1) Lib A ne peut pas cesser de fonctionner.
    2) Lib A doit faire plusieurs réinitialisation complets au cours de son cycle de vie.
    3) Lib A utilise des ressources bas niveau: fichiers, connexion réseau, vfs entoures par des wrappers...

    En utilisant le RAII, lors de la réinitialisation du contexte, les destructeurs des wrappers peuvent donc lancer des exception, je vois plusieurs solutions mais aucune satisfaisante:

    Laisser l'exception se propager: le wrapper étant détruit avant la propagation, on ne peut plus rien faire pour la ressource toujours ouverte, l'application pouvant rester chargée sur une durée indéterminée(jours/semaines) le risque de starvation augmente au fil du temps.
    Catch exception dans le destructeur et garder le handler de la ressource dans un systeme async qui va réessayer le nettoyage? complexe mais dans le domaine du possible.
    noexept: terminer le processus va a l'encontre de la contrainte 1


    Quels sont vos conseils?

    Merci.

  2. #2
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    Salut,

    D'abord et avant tout: un destructeur ne peut pas lancer d'exception:

    Le destructeur est automatiquement appelé à la fin de la durée de vie d'un objet, pour permettre la libération correcte des ressources.

    Y compris lorsque la fin de la durée de vie de l'objet en question survient suite à la survenue d'une exception.

    Il n'y a donc absolument aucune raison pour qu'il lance une exception, car, au pire, si une ressource n'existe pas (parce qu'une exception est survenue lors de sa création), elle n'a aucun besoin d'être détruite

    Ce n'est que lors de l'exécution (création comprise) que tu peux -- effectivement -- te trouver face à une situation exceptionnelle (comprends: dont tout le monde espère qu'elle ne se présentera jamais tout en étant bien conscient qu'il n'y rien faire pour éviter qu'elle ne se présente) qui pourrait nécessiter un abandon de la tâche (et un nouvel essai éventuel).

    Ce que tu dois donc faire, dans les destructeurs des éléments qui utilisent des ressources bas niveau, c'est uniquement t'assurer que tu va -- effectivement -- libérer correctement les ressources.

    Mettons que tu aies une structure fournie par une DLL écrite en C. Tu seras donc "logiquement" face à une structure, une fonction pour créer la ressource, une autre pour libérer la ressource et "un certain nombre" (on va dire deux, par facilité ) de fonction permettant de l'utiliser.

    Cela te mettra donc dans une situation proche de
    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
    23
    struct MyStruct{
         /* peu importe ce qu'elle contient, seule compte la logique ;) */
    };
    MyStruct createStruct(/* paramètres */){
         MyStruct * ptr = malloc(sizeof(MyStruct));
         /* ... */
         return MyStruct;
    }
    void destroyMyStruct(MyStruct * ptr){
         if(ptr){
             /* ... */
             free(ptr);
         }
    }
    int firstUse(MyStruct * ptr /*,autres paramètres */){
         /*on considère qu'un renvoi différent de zero indique une erreur */
         return UNKNOWN_PROBLEM; 
    }
     
    int secondUse(MyStruct * ptr /*,autres paramètres */){
         /*on considère qu'un renvoi différent de zero indique une erreur */
         return UNKNOWN_PROBLEM; 
    }
    Les seules situations dans lesquelles "quelque chose d'inattendu" peu survenir sont:
    • dans la fonction createMyStruct si la structure ne peut pas être créée
    • dans la fonction firstUse
    • dans la fonction secondUse

    Quant à la fonction destroyMyStruct, ben, elle ne peut pas échouer.

    Tu vas donc créer un wrapper dont le constructeur fera appel à createMyStruct, dont le destructeur fera appel à destroyMyStruct, et dont deux fonctions membres feront respectivement appel à firstUse et à secondUse.

    Tu vas, dans l'idéal, indiquer que ce wrapper ne peut être ni copiée ni assignée
    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
    23
    24
    25
    26
    27
    28
    29
    30
     
    class Wrapper{
    public:
        Wrapper(/* paramètres */){
            ptr = createMyStruct(/* ... */);
            if(! ptr) {
                 throw std::runtime_error("unable to create MyStruct");
            }
        }
        Wrapper(Wrapper const &) = delete;
        Wrapper & operator=(Wrapper const &) = delete;
        ~Wrapper(){ // à la fin de la durée de vie, la ressource est libérée quoi qu'il arrive 
            destroyMyStruct(ptr);
        }
        void callFirstUse(/* paramètres */){
            assert(ptr && "ressource not allocated"); // ca ne devrait pas arriver, mais... sait-on jamais
            if(firstUse(/* param§tres*/)){ // ici, j'ai fait en sorte de ne pas renvoyer 0 en cas d'erreur
                throw std::runtime_error("something bad when calling firstUse");
            } 
        }
        void callSecondUse(/* paramètres */){
            assert(ptr && "ressource not allocated"); // ca ne devrait pas arriver, mais... sait-on jamais
            if(firstUse(/* param§tres*/)){ 
                throw std::runtime_error("something bad when calling firstUse");
            } 
        }
    private:
        /* notre si précieuse ressource */
        MyStruct * ptr;
    };
    Une fois que cela est fait, ben, tu n'a plus à t'en faire pour la gestion de MyStruct dans ton code, car la fonction createMyStruct sera automatiquement appelée à chaque fois que tu créera un wrapper, et la fonction destroyMyStruct sera automatiquement appelée (au travers du destructeur) à chaque fois que l'on atteindra l'accolade fermante correspondant au bloc dans lequel le wrapper a été créé

    Cela pourrait prendre la forme d'une fonction qui ne s'occupe pas de gérer les exception:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void foo(){
        Wrapper w; // creation de la ressource (lance peut-être une exception)
        w.callFirstUse(/* ... */) ; // lance peut-être une exception
        w.callSecondUse(/* ... */) ; // lance peut-être une exception    
    } // le destructeur de w est automatiquement appelé ici, même si une exception est lancée
     
    int main(){
        try{
            foo();
        }
        catch(std::exception & e){
            std::cout<<e.what()<<"\n";
        }
    }
    ou tu peux envisager de gérer les exceptions lors des appels, si cela fait du sens:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int main(){
        try{    
            Wrapper w; // creation de la ressource (lance peut-être une exception)
            w.callFirstUse(/* ... */) ; // lance peut-être une exception
            w.callSecondUse(/* ... */) ; // lance peut-être une exception    
        } // le destructeur de w est automatiquement appelé ici, même si une exception est lancée
        catch(std::exception & e){
            std::cout<<e.what()<<"\n";
        }
    }
    Quoi qu'il en soit, le destructeur de w sera automatiquement et systématiquement appelé au niveau de l'accolade fermante du bloc qui contient sa création. C'est garanti par la norme . Tu n'as donc pas besoin de t'en inquiéter

    NOTA: il existe quantité d'autres solutions permettant d'obtenir un résultat identique. J'aurais tout aussi bien pu utiliser std::unique_ptr et adapter le deleter pour qu'il foncitonne correctement, par exemple

    Le fait essentiel est que, une fois ta capsule RAII mise au point, tu n'as plus à t'inquiéter du fait que les ressources seront correctement libérées. Au pire, tu peux envisager plusieurs traitements différents en fonction de la situation qui a posé problème.

    Rien ne t'empêcherait pour y arriver, par exemple, de faire dériver les exceptions MyStructCreateException, MyStructFirstUseException et MyStructSecondUseException de std::runtime_error (ou de toute autre exception spécifique de ton choix) et de les faire lancer selon les situation, ce qui te permettrait de les récupérer "séparément"
    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

  3. #3
    Membre Expert Avatar de yildiz-online
    Homme Profil pro
    Architecte de domaine
    Inscrit en
    Octobre 2011
    Messages
    1 447
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Architecte de domaine

    Informations forums :
    Inscription : Octobre 2011
    Messages : 1 447
    Par défaut
    Salut,

    Merci j'avais bien compris ces concepts, ce qui me pose problème c'est bien le

    Le fait essentiel est que, une fois ta capsule RAII mise au point, tu n'as plus à t'inquiéter du fait que les ressources seront correctement libérées. Au pire, tu peux envisager plusieurs traitements différents en fonction de la situation qui a posé problème.
    D'accord mais les traitements ne sont pas forcement possibles durant le cycle de destruction de l'objet :un exemple simple, on efface un fichier temporaire, mais un utilisateur malavisé l'a ouvert dans un autre logiciel, bloquant son effacement, le wrapper lance donc une exception, et dans ce cas, comment garantir l'état une fois la destruction terminée sans propager d'exception pour notifier soit l'utilisateur, soit le système qu'un comportement erroné qui ne peut être résolu a ce moment est arrivé et doit être pris en charge?

  4. #4
    Expert éminent
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 395
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 41
    Localisation : France

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

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 395
    Par défaut
    Pour le cas d'un fichier temporaire, je dirais que l'absence de destruction ne devrait pas être une condition "bloquante" (s'il empêche la création du nouveau fichier par contre, on le saura à un moment où on peut traiter l'erreur normalement).
    Mais je comprends le problème: Effacement de données confidentielles, logout d'un service web, tout ça relève d'un "nettoyage" qui peut néanmoins échouer. Mais pour moi, à part pour le cas de "on réessaie quelques secondes plus tard", c'est une situation à laquelle le code ne peut rien. Cela ne peut être résolu que par un message à l'utilisateur après-coup.

    C'est le genre de choses pour lesquelles je garderais dans la mémoire de mon objet RAII une référence à un objet "log d'erreurs" qu'on remplirait en cas de problème lors de la destruction.
    Code C++ : 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
    23
    24
    25
    26
    27
    28
    class MaRessource
    {
    	Erreurs& erreurs;
     
    	~MaRessource()
    	{
    		try
    		{
    			Nettoyage();
    		}
    		catch(std::exception &ex)
    		{
    			erreurs.Probleme(ex);
    		}
    	}
    };
     
     
    void MonCode()
    {
    	Erreurs erreurs;
    	{
    		MaRessource res { erreurs };
    		Faire();
    	}
    	if(!erreurs.EstVide())
    		std::cout << "Oups!" << erreurs << std::endl;
    }
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  5. #5
    Membre Expert Avatar de yildiz-online
    Homme Profil pro
    Architecte de domaine
    Inscrit en
    Octobre 2011
    Messages
    1 447
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Architecte de domaine

    Informations forums :
    Inscription : Octobre 2011
    Messages : 1 447
    Par défaut
    Avant toute chose, petite correction de mon message précèdent:

    Merci j'avais bien compris ces concept
    Doit se lire

    Merci de ta réponse.
    J'avais bien compris ces concepts...
    La mauvaise ponctuation change totalement le ton que je désirais employer.

    Pour en revenir a la discussion

    Merci Medinoc,

    On est bien d'accord que la destruction ne peut pas être bloquante, sinon le RAII n'a plus aucun sens.

    En tous cas j'ai ma réponse, le RAII n'est pas la silver bullet du nettoyage, il reste des failles pour les cas exceptionnels, ca rejoint les articles que j'avais lu sur le sujet.

    L'idée d'une "boite a erreur" est pas mal pour alimenter le système async de traitement que j'espérais éviter.(par flemme, je l'avoue)

    Merci pour votre précieuse aide a tous les deux.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. RAII et exception
    Par yan dans le forum C
    Réponses: 19
    Dernier message: 04/12/2007, 23h31
  2. Exception & Try..catch
    Par PurL dans le forum C++Builder
    Réponses: 2
    Dernier message: 11/12/2002, 15h35
  3. Réponses: 3
    Dernier message: 01/11/2002, 14h30
  4. Réponses: 5
    Dernier message: 12/06/2002, 15h12
  5. c: gestion des exceptions
    Par vince_lille dans le forum C
    Réponses: 7
    Dernier message: 05/06/2002, 14h11

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