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 :

Votre avis sur la gestion d'erreur avec macros throwIf(), throwIfNot(), throwError()


Sujet :

Langage C++

  1. #1
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut Votre avis sur la gestion d'erreur avec macros throwIf(), throwIfNot(), throwError()
    Bonjour,

    La gestion des erreurs est une composante aussi importante que délicate dans une appli. Et je sollicite votre opinion sur 3 macros que je viens d'adopter dans un de mes projets.

    Elles ont pour but de simplifier la gestion d'erreurs internes au moyen d'exception. Leur design vise à satisfaire les contraintes suivantes:
    1. simplifier au maximum le test et de remontée d'erreur (éliminer les if)
    2. faciliter l'écriture de messages d'erreurs détaillés (inclure les code d'erreur...)
    3. permettre la recherche automatique des erreurs dans le source (au moyen d'un script python...) afin de les documenter
    4. permettre la traduction des messages d'erreur si on le souhaite
    5. permettre d'inclure l'emplacement dans le source (fichier, ligne...) où l'erreur a été émise


    Ces points réunis m'ont convaincu de la pertinence d'utiliser des macros que j'ai appelé throwError, throwIf et throwIfNot. Elles s'utilisent ainsi:

    Code cpp : 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
    void saveToFile(const gp::Message & msg, const fs::path & outputName) {
        std::ofstream file(outputName, std::ios::binary);
        throwIfNot(file, "Failed to create file {0}", outputName);
     
        std::string outputText;
        if (outputName.extension() == ".bin") {
            throwIfNot(msg.SerializeToString(&outputText),
                "Failed to encode to raw data"
            );
        }
        else if (outputName.extension() == ".txt") {
            throwIfNot(gp::TextFormat::PrintToString(msg, &outputText),
                "Failed to encode to text"
            );
        }
        else {
            throwError("Unsupported output file type {0}", outputName.extension());
        }
     
        throwIfNot(file << outputText,
            "Failed to write to file {0}", outputName
        );
    }

    Voilà pour "l'API" de ces macros. Voici mon implémentation actuelle, basée sur la fmtlib:

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <fmt/format.h>
    #include <fmt/ostream.h>
     
    #define throwError(...)\
        throw std::runtime_error{ fmt::format(__VA_ARGS__) }
    #define throwIf(cond, ...)\
        for (;(cond);) {\
            throwError(__VA_ARGS__);\
        }
    #define throwIfNot(cond, ...)\
        for (;!(cond);) {\
            throwError(__VA_ARGS__);\
        }

    On pourrait de façon assez similaire se baser sur Qt, et plus précisément QObject::tr() afin de rendre les messages d'erreur traduisibles. Il n'y aurait alors que la syntaxe des paramètres acceptés qui change ("{1}" => "%1").

    En ce qui me concerne, je ne suis pas fan de la création de moulte types d'exceptions. Je préfère ne lancer que des std::runtime_error et ne catcher que des std::exception. C'est à ce jour le seul souci que je vois avec ces macros : la difficulté de modifier au cas par cas le type de l'erreur levée. Mais pour moi c'est plutôt une bonne chose car j'utilise les exceptions pour remonter des erreurs critiques qui ne nécessitent (ne peuvent) pas être corrigées, mais simplement signalées. Je trouve en effet la gestion d'erreurs via une hiérarchie d'exceptions plus complexe, mais c'est un autre débat.

    Qu'en pensez-vous ?

    PS: l'utilisation de for(;;) dans la macro est une habitude prise vis à vis d'un warning VC++ sur le test de conditions qui est toujours vraie (si on passe "true" par exemple) mais c'est peut-être pas pertinent ici

  2. #2
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 469
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 469
    Points : 6 102
    Points
    6 102
    Par défaut
    Pourquoi pas.

    Par contre, pour le code des macros, à la place de :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    #define throwIf(cond, ...)\
        for (;(cond);) {\
            throwError(__VA_ARGS__);\
        }
    il faudrait écrire soit ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    #define throwIf(cond, ...)\
        if(cond)\
            throwError(__VA_ARGS__);\
        else\
            (void)0
    soit cela :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    #define throwIf(cond, ...)\
        do {\
            if(cond)\
                throwError(__VA_ARGS__);\
        } while(false)
    C'est pour pouvoir compiler ce genre de code :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        throwIf(paramètres...);
    else
        doSomething();
    Ces deux astuces syntaxiques sont expliquées dans 2 entrées de la FAQ C++ de isocpp.org, respectivement ici et .

  3. #3
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Qu'est-ce qui va pas avec l'utilisation de for(;;) ?

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }
    else
        doSomething();
    La différence c'est que sous VC++, ça passe sans problème alors que do {} while (false) ou if (false) else {} peuvent produire le warning 4127 (conditional expression is constant).

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    throwIf(sizeof(int) == 4, "oups"); // exemple pour générer le warning 4127
    Après, comme expliqué vite fait dans mon PS, c'est peut-être souhaitable dans le cas de ces macros de générer ce warning, alors ok pour faire comma la FAQ dit qu'il faut faire

    Mais ce qui m'intéresse c'est votre avis sur comment mieux gérer nos erreurs en C++.

  4. #4
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    Comment gérer les erreurs?
    RAII.
    encore RAII.
    toujours RAII.
    utiliser le système de typage, avec des mots clés comme class et explicit.

    S'il n'est pas possible de compiler une situation erronée, alors la situation ne peut pas être erronée, et l'erreur n'est pas à controler.

    Il suffit alors de valider les entrées utilisateurs proprement, et de lui remonter l'erreur proprement.
    Pour une lecture de fichier, une exception à l'analyse.
    Nul besoin d'exception si on a une interface interactive.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  5. #5
    Membre éclairé

    Profil pro
    Inscrit en
    Décembre 2013
    Messages
    393
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2013
    Messages : 393
    Points : 685
    Points
    685
    Par défaut
    @ternel

    Comment tu gères un disque qui était accessible lorsque l'utilisateur a entré le nom du fichier, mais devient indisponible avant ou pendant la lecture ?

    Il faut remettre les choses a leur place quand même :

    - RAII n'est pas un mécanisme de gestion des erreurs. C'est un mécanisme de gestion automatique des ressources. Qui est effectivement très pratique en cas d'erreur ou d'exception, mais pas uniquement.

    - les exceptions ne sont pas un mécanisme des gestion des erreurs. C'est un mécanisme de gestion des choses qui arrivent (ou qui devrait arriver) exceptionnellement. Cela peut être des erreurs, mais peut on considérer qu'un disque qui n'est plus accessible est une "erreur" ? (Difficile de donner une definition claire de "erreur", mais on peut trouver "faute , méprise" sur wiki. Utiliser un ofstream invalide serait une erreur, mais qu'un disque ne soit plus accessible est une situation qui peut arriver et qui doit être prise en compte par le logiciel).

    Pas sur de comprendre la remarque sur "class" et "explicit".

    @Aurelien

    Je pense que for est ok. C'est pareil qu'un do-while

  6. #6
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 469
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 469
    Points : 6 102
    Points
    6 102
    Par défaut
    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Qu'est-ce qui va pas avec l'utilisation de for(;;) ?

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }
    else
        doSomething();
    Ce que je voulais pointer du doigt, c'était ça :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }; // Le dernier point-virgule provoque une erreur de compilation.
    else
        doSomething();
    Le problème vient des crochets.

    Après réflexion, tu peux faire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    #define throwIf(cond, ...)\
        for (;(cond);)\
            throwError(__VA_ARGS__)

  7. #7
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 069
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 069
    Points : 12 113
    Points
    12 113
    Par défaut
    Je vois pas trop la différence avec la MACRO VERIFY et le fait d'avoir un seul type d'exception est rédhibitoire.

    Je ne catche que les que je sais gérer, et n'avoir qu'un seule type d'exception, c'est la loose complète !!!

    Mais pour moi c'est plutôt une bonne chose car j'utilise les exceptions pour remonter des erreurs critiques qui ne nécessitent (ne peuvent) pas être corrigées, mais simplement signalées.
    Cela rend son utilisation impossible dans un projet qui utilise toutes les fonctionnalités des exceptions.

    simplifier au maximum le test et de remontée d'erreur (éliminer les if)
    VERIFY fait pareil.

    faciliter l'écriture de messages d'erreurs détaillés (inclure les code d'erreur...)
    Les exceptions ont déjà ce champ, et même un message d'erreur.

    permettre la recherche automatique des erreurs dans le source (au moyen d'un script python...) afin de les documenter
    Heu, pourquoi faire, vu que la stacktrace donne les numéros de ligne et les chemins vers les fichiers ?

    permettre la traduction des messages d'erreur si on le souhaite
    Traduire un message d'erreur qui n'est destiné qu'au développeur, est vraiment nécessaire ???

    permettre d'inclure l'emplacement dans le source (fichier, ligne...) où l'erreur a été émise
    La stacktrace serait bien plus utile est il y a déjà des trucs, même si c'est plateforme dépendant.
    http://stackoverflow.com/questions/3...tack-in-c-or-c

  8. #8
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par ternel Voir le message
    Il suffit alors de valider les entrées utilisateurs proprement, et de lui remonter l'erreur proprement.
    C'est le but de ce thread : clarifier le "il suffit" et comment faire ça "proprement". Et je ne demande qu'à ce que tu me montres comment le RAII (qui est un idiome donc au niveau local et non global...) peut simplifier encore plus l'écriture et l'utilisation de ma fonction saveToFile()

    D'après mon expérience, la gestion propre des erreurs n'est pas du tout évidente car le plus imbriqué des appels est susceptible de devoir signaler un problème à l'utilisateur dans sa langue et dans son contexte (échouer à ouvrir le fichier qu'il a demandé n'est pas pareil qu'échouer à lire un fichier de conf interne). Sachant qu'on veut aussi disposer de détails techniques pour l'analyse côté dev (fichiers de logs...) et pouvoir documenter la liste des messages possible pour le support. Déjà ça, c'est coton à faire "proprement". Mais alors quand l'erreur se produit dans un autre thread ou une fonction distante appelée en RPC...

    En fait, quand je découvre un code source, la première chose que je regarde pour me faire une idée macro de sa qualité est sa gestion d'erreur. Pour moi c'est la colonne vertébrale autour de laquelle s'architecture tout le reste. Et bien souvent c'est à grands coups de if imbriqués. Autant dire que c'est très lourd à lire et écrire!

    Là, avec ces 3 petites macros, j'ai réussi à diviser par deux le nombre de lignes de code initiales et à rendre les erreurs plus verbeuses. Je me demande si on peut faire mieux.

    Citation Envoyé par Pyramidev Voir le message
    Ce que je voulais pointer du doigt, c'était ça :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }; // Le dernier point-virgule provoque une erreur de compilation.
    else
        doSomething();
    Le problème vient des crochets.
    Ah ok. Merci.

  9. #9
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 275
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 275
    Points : 10 985
    Points
    10 985
    Par défaut
    Plutôt que des macros, pourquoi ne pas en faire des fonctions inlines template variadiques ?
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  10. #10
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    la seule raison pour en faire des macros serait d'inclure le code de l'expression de test dans le message d'erreur, puisque c'est la seule chose qu'une template ne puisse pas faire.
    Mais qui a besoin de "connection.opened()" dans le message. "Connection is not opened" est plus humain.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  11. #11
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par Luc Hermitte Voir le message
    Plutôt que des macros, pourquoi ne pas en faire des fonctions inlines template variadiques ?
    Pour include l'emplacement de l'erreur (__FILE__ + __LINE__). Après dans mon exemple ce n'est pas le cas donc on pourrait faire sans en effet! bon en fait les macros ont une coloration syntaxique différente dans les IDE que j'utilise ce qui est pratique pour voir les cas d'erreurs, c'est la vrai raison en fait .

    J'ai oublié de préciser un avantage important pour moi d'encapsuler tous mes tests d'erreur dans de telles fonctions / macros :
    - la possibilité dans des tests de simuler automatiquement tous las cas d'erreur et ainsi valider que tout fonctionne (cleanup...)

    Il suffit pour cela d'exécuter en boucle le même code en faisant à chaque fois échouer "pour de faux" l'appel suivant à throwIf[Not].

  12. #12
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 069
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 069
    Points : 12 113
    Points
    12 113
    Par défaut
    car le plus imbriqué des appels est susceptible de devoir signaler un problème à l'utilisateur dans sa langue et dans son contexte
    NON, ce n'ai qu'au code traitant l'exception (donc le plus externe) qu'incombe la responsabilité d'éventuellement prévenir l'utilisateur du problème, et selon un langage INTELLIGIBLE pour l'utilisateur de l'application.
    Un numéro d’incident correspondant à un ticket au support est toujours plus rassérénant qu'une liste cabalistique de chiffre ou le nom du fichier qu'il ne connait ni d'Eve ni des dents.

    échouer à ouvrir le fichier qu'il a demandé n'est pas pareil qu'échouer à lire un fichier de conf interne
    D'où l'utilisation de plusieurs types d'exceptions, pour que le traitement potentiel de l'erreur soit simple et efficace.

    Sachant qu'on veut aussi disposer de détails techniques pour l'analyse côté dev (fichiers de logs...)
    Sachant que les exceptions sont bardées, potentiellement, d'autant de champs que nécessaire, si on prend la peine de spécialiser leur type et de leur faire implémenter, si nécessaire, une API compatible avec le framework de Log utilisé ; il n'y a donc aucun problème à avoir tous ces détails. Si l'on prend la peine de correctement typer ces exceptions.

    et pouvoir documenter la liste des messages possible pour le support
    Comme les seuls messages à documenter sont ceux affichée dans l'IHM, donc dans le traitement des exceptions, ils sont bien moins nombreux et orientés utilisateurs.

    Mais alors quand l'erreur se produit dans un autre thread ou une fonction distante appelée en RPC...
    Vous n'avez pas à faire subir cela à l'utilisateur et les framework de Logs indiquent les threadID, voir même des ActivityID pour suivre les actions sur plusieurs programmes sur des machines différentes, etc...

    sa qualité est sa gestion d'erreur
    Et moins il y en a mieux c'est, car, à moins d'un code de tâcheron, c'est que c'est l'architecture qui gèrent les erreurs et pas le code.

    Pour moi c'est la colonne vertébrale autour de laquelle s'architecture tout le reste.
    Pour moi, la colonne vertébrale, c'est l'architecture, la gestion des erreurs fait partie intégrante de l'architecture.

    Et bien souvent c'est à grands coups de if imbriqués. Autant dire que c'est très lourd à lire et écrire!
    Encore des personnes qui ont zappé la fin des mauvais livres de C++ et qui code comme en C.
    Les exceptions, cela doit être dans les premiers chapitres des cours de C++.

    j'ai réussi à diviser par deux le nombre de lignes de code initiales
    Moins de code = moins de bugs.
    Mais il faut aussi que le code résultant soit plus lisible.

    Je ne vois toujours pas l'avantage de ces MACROS sur la VERIFY (des MFC mais facilement transférable).

  13. #13
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par bacelar Voir le message
    D'où l'utilisation de plusieurs types d'exceptions, pour que le traitement potentiel de l'erreur soit simple et efficace.
    Comment le code le plus externe peut-il gérer toutes les erreurs de tous les composants de façon simple et générique? Je pense que la multiplication des types d'exceptions (avec son râteliers de try...catch) va à l'encontre de cette possibilité et complexifie la gestion des erreurs. Qui s'est déjà amusé à catcher en série std::out_of_range, logic_error, runtime_error, invalid_argument ou que sais-je encore? En dehors de bad_alloc, j'en ai jamais vu l'intérêt. Mais je veux bien voir du code qui gagne en élégance à faire ainsi.

    Si ce n'est pas le cas, mais alors à quoi bon créer tous ces types? Je n'en vois qu'un seul : les conserver privés et s'en servir uniquement en interne pour imposer un certain formalisme au niveau des données recueillies (nom du fichier qu'on n'a pas pu lire...) Mais dans la pratique c'est lourd et overkill : c'est quoi le nom exact déjà, FileReadException ? c'est dans quel include ? quelle différence avec FileIOError créé par un autre développeur? Ah mais peut-être que j'arrive pas à lire parce que c'est une FileNotFoundException en fait.

    Donc pour ma part j'ai arrêté cette usine à gaz et maintenant je vise la simplicité : je génère le message d'erreur final sur le lieu du problème en ayant toujours en tête qu'il peut potentiellement être affiché à l'utilisateur. Ca scale très bien, ça ne brise pas l'encapsulation, et c'est compatible avec le plus basique des exceptions handler : pas besoin d'imposer au développeur de mettre à jour son code parce que j'ai rajouté de nouvelles erreurs au miens (Open/Close principle).

    Citation Envoyé par bacelar Voir le message
    Sachant que les exceptions sont bardées, potentiellement, d'autant de champs que nécessaire, si on prend la peine de spécialiser leur type et de leur faire implémenter, si nécessaire, une API compatible avec le framework de Log utilisé ; il n'y a donc aucun problème à avoir tous ces détails. Si l'on prend la peine de correctement typer ces exceptions.
    Nous y sommes : contamination du code utilisateur, rupture de l'encapsulation, du principe Open/Close... Je n'ai pas précisé : dans l'implémentation réelle de mes "macros", le message de log est enregistré au moment du throw, pas dans le catch : "ce qui est fait n'est plus à faire".

    Citation Envoyé par bacelar Voir le message
    les framework de Logs indiquent les threadID, voir même des ActivityID pour suivre les actions sur plusieurs programmes sur des machines différentes, etc...
    Pour les threads, le challenge est de transmettre l'erreur en asynchrone depuis un worker thread vers le thread principal (GUI) qui affiche les erreurs.

    Citation Envoyé par bacelar Voir le message
    Pour moi, la colonne vertébrale, c'est l'architecture, la gestion des erreurs fait partie intégrante de l'architecture.
    Aujourd'hui j'ai le label "architecte" sur mon CV, ça doit être pour cela que je me méfie de ce terme très abstrait qui permet de dissimuler bien du bullshit. "comment sont gérées les erreurs?", là tout de suite on rentre dans la partie douloureuse.

    Citation Envoyé par bacelar Voir le message
    Encore des personnes qui ont zappé la fin des mauvais livres de C++ et qui code comme en C.
    Les exceptions, cela doit être dans les premiers chapitres des cours de C++.
    C'est un vieux débat : exception vs code de retour (C++ a fait machine arrière avec les spécificateur d'exceptions, en embarqué elles sont souvent bannies, Go a fait le choix de ne pas avoir d'exceptions...). Ce qui est sûr c'est que les deux sont utiles et qu'aucun des deux n'est la panacée. Je trouve qu'ils gagnent à être combinés.

    D'autant plus que ce sont des concepts orthogonaux. Exemple:

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define ERROR_FAILURE 1
     
    void f() {
        throw ERROR_FAILURE;
    }
     
    std::exception g() {
        return std::exception{ "Failure!" };
    }

    Citation Envoyé par bacelar Voir le message
    Je ne vois toujours pas l'avantage de ces MACROS sur la VERIFY (des MFC mais facilement transférable).
    J'ai mon propre équivalent:
    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    #define ASSERT_ALWAYS(cond) \
        if (cond) {} \
        else { \
            DEBUG_BREAK();\
            static constexpr auto s_fileName = core::extractFileName(__FILE__); \
            throwError("Internal error (ASSERT_ALWAYS in file '{}' at line {})", s_fileName, __LINE__);\
        }

    Ca sert à valider les préconditions. Et c'est tout.

    J'en ai usé et abusé dans mon code de ce genre de macros, au point de traiter bien plus que les préconditions avec, car c'est très pratique. Mais quand ça pète, çe le fait pas d'afficher à l'utilisateur un message du genre "assertion failed: file.isOpen() != true" au lieu de "failed to open file 'xxx'".

    C'est comme ça que j'en suis arrivé à ce que je propose dans ce thread en complément de VERIFY (qui plus est, le message texte associé rend le contrôle plus lisible et dispense de mettre un commentaire).

  14. #14
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par Luc Hermitte Voir le message
    Plutôt que des macros, pourquoi ne pas en faire des fonctions inlines template variadiques ?
    Au passage, j'ai pu constater que le remplacement de mes macros par des fonctions variadiques a provoqué des erreurs de compilation sur le test de fichier (opérateur bool explicite):

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    std::ifstream file("input.txt");
    throwIfNot(file, "oops"); // échec de conversion vers bool

    Alors que ce code passait en mode macro (if (file)).

    Je me suis résolu à créer des surcharges spécifiques:

    Code cpp : 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
    namespace core {
        bool isOk(std::ostream & stream) {
            return !stream.fail();
        }
        bool isOk(std::istream & stream) {
            return !stream.fail();
        }
    }
     
    template <typename ...Args>
    void throwIfNot(std::ostream & stream, Args&&... args) {
        throwIfNot(core::isOk(stream), std::forward<Args>(args)...);
    }
    template <typename ...Args>
    void throwIfNot(std::istream & stream, Args&&... args) {
        throwIfNot(core::isOk(stream), std::forward<Args>(args)...);
    }

  15. #15
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    C'est qu'il suffirait d'un static_cast<bool>(file).

    La condition du if, comme celles du while et du for, sont des contextes de conversion explicite en booléen.
    Le passage d'un argument à un appel de fonction ne l'est pas.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    template <typename ...Args>
    void throwIfNot(std::ostream & stream, Args&&... args) {
        throwIfNot(static_cast<bool>(stream), std::forward<Args>(args)...);
    }
    Cela dit, tu pourrais être plus général, et permettre tous les basic_ostream<...> plutot que seulement ostream. Ca serait utile pour les wostream, par exemple.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  16. #16
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 069
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 069
    Points : 12 113
    Points
    12 113
    Par défaut
    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Comment le code le plus externe peut-il gérer toutes les erreurs de tous les composants de façon simple et générique?
    On ne gère pas les erreurs de manière "générique", soit vous savez la traiter complètement et vous la catchez via son type, soit vous ne faites rien.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Je pense que la multiplication des types d'exceptions (avec son râteliers de try...catch)
    Un "râteliers de try...catch", ça sent pas bon. Vous ne catchez que les types que vous savez gérés donc un nombre très restreint pour un lieu du code.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    va à l'encontre de cette possibilité et complexifie la gestion des erreurs.
    Tout à fait, on ne catche que ce que l'on sait gérer, donc simple.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Qui s'est déjà amusé à catcher en série std::out_of_range, logic_error,invalid_argument ou que sais-je encore?
    Pas moi, quel intérêt de catcher des trucs qui correspond à des erreurs de programmation. A part allonger les sessions de debugging ?
    Pour les tests unitaires, c'est différent, mais c'est pas le même contexte d'exécution.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    runtime_error... . En dehors de bad_alloc, j'en ai jamais vu l'intérêt. Mais je veux bien voir du code qui gagne en élégance à faire ainsi.
    Qu'entendez-vous par "élégance" ?
    Le code sait gérer cette exception ou pas, point barre.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Si ce n'est pas le cas, mais alors à quoi bon créer tous ces types? Je n'en vois qu'un seul : les conserver privés et s'en servir uniquement en interne pour imposer un certain formalisme au niveau des données recueillies (nom du fichier qu'on n'a pas pu lire...) Mais dans la pratique c'est lourd et overkill : c'est quoi le nom exact déjà, FileReadException ? c'est dans quel include ? quelle différence avec FileIOError créé par un autre développeur? Ah mais peut-être que j'arrive pas à lire parce que c'est une FileNotFoundException en fait.
    Les types d'exceptions, c'est comme les types de retour ou les types de paramètre, c'est à concevoir avec soin. Ils doivent renforcer l'encapsulation du module. S'il y a un problème sur l'initialisation d'un module, vous devez avoir une exception sur l'initialisation. Que l'initialisation se base sur un fichier de configuration ou une base de données, la base de registre, la mécanique de fallback est la même.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Donc pour ma part j'ai arrêté cette usine à gaz et maintenant je vise la simplicité : je génère le message d'erreur final sur le lieu du problème en ayant toujours en tête qu'il peut potentiellement être affiché à l'utilisateur.
    C'est pas généralisable et vous vous faites de faux problèmes.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Ca scale très bien, ça ne brise pas l'encapsulation, et c'est compatible avec le plus basique des exceptions handler : pas besoin d'imposer au développeur de mettre à jour son code parce que j'ai rajouté de nouvelles erreurs au miens (Open/Close principle).
    Heu, là, c'est nimp, ça brise pas l'encapsulation ? Bin oui, vous n'en avez pas, c'est vraiment pas mieux.
    Il n'y aura pas de problème de mise à jour du code utilisateur. Soit le problème se règle comme avant => une exception sous classe de celle d'avant; soit le problème ne se règle pas comme avant et le code plantera, comme il se doit !!!


    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Nous y sommes : contamination du code utilisateur, rupture de l'encapsulation, du principe Open/Close... Je n'ai pas précisé : dans l'implémentation réelle de mes "macros", le message de log est enregistré au moment du throw, pas dans le catch : "ce qui est fait n'est plus à faire".
    Comme si le code qui détecte une anomalie sait comment présenter le problème à l'utilisateur, LOL. Non !!!


    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Pour les threads, le challenge est de transmettre l'erreur en asynchrone depuis un worker thread vers le thread principal (GUI) qui affiche les erreurs.
    Mais pourquoi faire ??? Il s'en cogne l'utilisateur que le thread XXX n'a pas réussi à atteindre le serveur YYY.
    Ce qu'il veut, c'est savoir si son action demandé c'est bien faite et les actions qu'il peut entreprendre pour que cet action puisse enfin s’exécuter en cas de problème.



    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Aujourd'hui j'ai le label "architecte" sur mon CV, ça doit être pour cela que je me méfie de ce terme très abstrait qui permet de dissimuler bien du bullshit. "comment sont gérées les erreurs?", là tout de suite on rentre dans la partie douloureuse.
    Pas si vous avez mis en place l'architecture qui va bien.


    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    C'est un vieux débat : exception vs code de retour (C++ a fait machine arrière avec les spécificateur d'exceptions, en embarqué elles sont souvent bannies, Go a fait le choix de ne pas avoir d'exceptions...). Ce qui est sûr c'est que les deux sont utiles et qu'aucun des deux n'est la panacée. Je trouve qu'ils gagnent à être combinés.
    Tout à fait, c'est pour cela qu'un VERIFY qui ne fait pas forcement des exceptions est bien mieux (fonction de #define par exemple).

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    D'autant plus que ce sont des concepts orthogonaux
    Quels concepts, SVP.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Ca sert à valider les préconditions. Et c'est tout.
    Pour valider les préconditions, c'est le rôle d'assert.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    J'en ai usé et abusé dans mon code de ce genre de macros, au point de traiter bien plus que les préconditions avec, car c'est très pratique.
    Donc vous l'utilisez dans un cas où il existe déjà une primitive standard et dans les autres cas, vous nous empêchez de correctement gérer les exceptions ? Si je résume bien ?

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Mais quand ça pète, çe le fait pas d'afficher à l'utilisateur un message du genre "assertion failed: file.isOpen() != true" au lieu de "failed to open file 'xxx'".
    Ni l'un ni l'autre. Affichage des choses qu'il peut faire ou affichage d'un numéro d’incident pour le suivi par le HelpDesk.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    C'est comme ça que j'en suis arrivé à ce que je propose dans ce thread en complément de VERIFY (qui plus est, le message texte associé rend le contrôle plus lisible et dispense de mettre un commentaire).
    ???

  17. #17
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 469
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 469
    Points : 6 102
    Points
    6 102
    Par défaut
    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Mais quand ça pète, çe le fait pas d'afficher à l'utilisateur un message du genre "assertion failed: file.isOpen() != true" au lieu de "failed to open file 'xxx'".
    Citation Envoyé par bacelar Voir le message
    Ni l'un ni l'autre. Affichage des choses qu'il peut faire ou affichage d'un numéro d’incident pour le suivi par le HelpDesk.
    A ce propos, que pensez-vous d'un truc dans ce genre-là ?
    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    class exception_with_translatable_message : public std::exception
    {
    public:
    	const char* what() const noexcept override
    	{
    		return "Translatable message.";
    	}
    	const wchar_t* getMessage(Language language) const noexcept
    	{
    		switch(language) {
    			case Language::French : return french ();
    			case Language::Spanish: return spanish();
    			default:                return english();
    		}
    	}
    private:
    	virtual const wchar_t* english() const noexcept = 0;
    	virtual const wchar_t* french()  const noexcept = 0;
    	//! \remark Mettre en virtuelle pure pour trouver où il manque les traductions en espagnol.
    	virtual const wchar_t* spanish() const noexcept { return english(); }
    };
     
    class exception_fail_to_open_file : public exception_with_translatable_message
    {
    public:
    	explicit exception_fail_to_open_file(const wchar_t* filePath) :
    		m_filePath(filePath)
    	{}
    private:
    	const wchar_t* english() const noexcept override
    	{
    		try {
    			return (L"Fail to open file \"" + m_filePath + L"\".").c_str();
    		} catch(...) {
    			return L"Fail to open file.";
    		}
    	}
    	const wchar_t* french() const noexcept override
    	{
    		try {
    			return (L"Echec d'ouverture du fichier \"" + m_filePath + L"\".").c_str();
    		} catch(...) {
    			return L"Echec d'ouverture d'un fichier.";
    		}
    	}
    	std::wstring m_filePath;
    };
     
    const wchar_t* genericErrorMessage(Language language) noexcept
    {
    	switch(language) {
    		case Language::French : return L"Erreur";
    		case Language::Spanish: return L"Error";
    		default:                return L"Error";
    	}
    }
     
    int main()
    {
    	const Language language = Language::English;
    	try
    	{
    		// ...
    		throw exception_fail_to_open_file(L"C:/foo/bar.txt");
    		// ...
    	}
    	catch(exception_with_translatable_message& e)
    	{
    		log(e.getMessage(Language::French), Log::Severity::Error);
    		showMessageBox(e.getMessage(language));
    	}
    	catch(std::exception& e)
    	{
    		log(e.what(), Log::Severity::Error);
    		showMessageBox(genericErrorMessage(language));
    	}
    	catch(...)
    	{
    		log(L"Exception qui ne derive pas de std::exception !", Log::Severity::Error);
    		showMessageBox(genericErrorMessage(language));
    	}
    	return 0;
    }
    Avec cette solution, il n'y a pas besoin d'écrire un catch pour chaque type d'exception simple comme exception_fail_to_open_file. Il suffit de faire un catch(exception_with_translatable_message& e) qui récupère d'un coup tout ce que l'on peut afficher proprement à l'utilisateur.
    Ensuite, dans catch(std::exception& e), si on craint que le message ne soit pas joli, on peut toujours décider de le cacher à l'utilisateur.

    Remarque :
    J'ai écrit un code simpliste avec des formats de messages d'erreur écrits en dur dans le programme, ce qui n'est pas adapté si on veut les changer sans recompiler le code.
    Mais on pourrait imaginer une variante où le le format du message (par exemple L"Fail to open file \"%1\".") est récupéré dans un fichier qui contient l'ensemble des messages d'erreurs.

    Edit 2016-12-12-22h22 : char remplacé par wchar_t.
    Edit 2016-12-13-00h04 : Pour respecter le SRP, j'aurais dû créer une classe translatable_message. Tant pis, je laisse mon code ainsi.

  18. #18
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 069
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 069
    Points : 12 113
    Points
    12 113
    Par défaut
    Comme je l'ai déjà indiqué, pour moi, ce n'est pas à l'exception de faire le travail de mise en forme et donc encore moins de faire la translation.

    Ces catch dans le main est l'exemple même de ce qu'il ne faut pas faire.
    A la rigueur la "catch(...)" si vous êtes dans un OS trop archaïque pour avoir des mécanismes de watchDog et/ou de coredump correctes, mais sans cet horrible "showMessageBox" et avec un throw de l'exception directement après.

    Les classes d'exception doivent avoir les champs nécessaires à leur éventuelle traitement, elles n'ont pas à gérer comment l'utilisateur les verra. C'est au code de traitement de savoir présenter la chose.

    Pour les logs techniques, l'utilisation de visualiseur de logs paramétrables permettent d'avoir toute la souplesse voulue.

  19. #19
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    A ce propos, que pensez-vous d'un truc dans ce genre-là ?
    Je ne pense pas que la traduction doive être gérée au niveau de l'exception (devoir modifier le code pour supporter chaque nouvelle langue n'est pas maintenable). En général on extrait les chaîne à traduire (via un script) et c'est au niveau du catch qu'il doit y avoir un mécanisme de traduction de la chaîne reçue. Le fait d'avoir une fonction wrapper à la throwIf facilite grandement l'écriture d'un tel script.

    On peut aussi faire un QObject::tr() au niveau de la fonction wrapper (si on utilise Qt) ce qui plaide à nouveau en faveur d'une fonction wrapper.

    Il y a un autre avantage (plus important) pour moi à wrapper tous mes appels (conditionnels) à throw: la possibilité de facilement simuler dans le code de test toutes les erreurs possibles (il suffit d'exécuter en boucle le code testé en faisant échouer à chaque nouvelle itération le throwIf suivant).

  20. #20
    Expert éminent sénior

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 749
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 749
    Points : 10 666
    Points
    10 666
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par ternel Voir le message
    C'est qu'il suffirait d'un static_cast<bool>(file).
    En fait mon code d'exemple qui test le stream est scindé en deux (.h/cpp) afin de ne pas provoquer l'inclusion inutile des streams dans le .h (seul <iosfwd> est inclus à ce niveau). C'est vrai que j'ai pas cherché à remettre ce point en cause.

    Merci pour la remarque sur wostream et ses copains.

Discussions similaires

  1. Votre avis sur la gestion d'emails en entreprise ?
    Par tempd6 dans le forum Emploi
    Réponses: 7
    Dernier message: 07/01/2016, 09h42
  2. Réponses: 1
    Dernier message: 27/06/2014, 14h50
  3. Votre Avis sur l'article : Le clustering avec Glassfish
    Par millie dans le forum Glassfish et Payara
    Réponses: 7
    Dernier message: 26/03/2010, 16h58
  4. Réponses: 5
    Dernier message: 27/10/2009, 19h06
  5. Votre avis sur les outils de gestion qualité du codage
    Par leminipouce dans le forum Qualimétrie
    Réponses: 1
    Dernier message: 19/10/2006, 21h00

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