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 :

Apprendre la Programmation Par Contrat (PpC) en C++


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Expert confirmé

    Avatar de Francis Walter
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2012
    Messages
    2 315
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Bénin

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

    Informations forums :
    Inscription : Février 2012
    Messages : 2 315
    Par défaut Apprendre la Programmation Par Contrat (PpC) en C++


    Je vous présente un premier épisode d'une série de trois billets de Luc Hermitte sur la Programmation Par Contrat (PpC) en C++. Cette première partie aborde les aspects théoriques de la Programmation Par Contrat en C++ : Partie 1/3 : Un peu de théorie.

    Dans la deuxième partie, il traitera des assertions puis en guise de conclusion, il présentera des techniques d'application de la PpC au C++ qu'il a croisées au fil des ans.



    Tous les meilleurs cours et tutoriels pour apprendre la programmation C++

  2. #2
    Community Manager

    Profil pro
    Inscrit en
    Avril 2014
    Messages
    4 207
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2014
    Messages : 4 207
    Par défaut Tutoriel pour apprendre la Programmation par Contrat en C++ - Les assertions
    Chers membres du club,

    J'ai le plaisir de vous présenter La deuxième partie de cette série de tutoriels de Luc Hermitte pour vous apprendre la programmation par contrat en C++. Dans ce second tutoriel, nous allons apprendre à utiliser les assertions.

    La première chose que l'on peut faire à partir des contrats, c'est de les documenter clairement. Il s'agit probablement d'une des choses les plus importantes à documenter dans un code source. Et malheureusement, trop souvent c'est négligé.
    Bonne lecture.

    Retrouvez les meilleurs cours et tutoriels pour apprendre la programmation C++
    Pour contacter les différents services du club (publications, partenariats, publicité, ...) : Contacts

  3. #3
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    Citation Envoyé par Luc Hermitte
    Comment peut-on détourner les assertions ? Tout simplement en détournant leur définition. N'oublions pas que les assertions sont des macros dont le comportement exact dépend de la définition de NDEBUG.

    Une façon assez sale de faire serait p.ex. :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    #if defined(NDEBUG)
    #   define my_assert(condition_, message_) \
           if (!(condition_)) throw std::logic_error(message_)
    #else
    #   define my_assert(condition_, message_) \
           assert(condition_ && message_)
    #endif
    Je cite BOOST_ASSERT qui est la version raffinée de cette technique.
    Par défaut, BOOST_ASSERT est un synonyme de assert.
    Par contre, quand la macro BOOST_ENABLE_ASSERT_HANDLER est définie, BOOST_ASSERT appelle la fonction :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    namespace boost
    {
      void assertion_failed(char const * expr, char const * function, char const * file, long line);
    }
    qui est déclarée dans Boost, mais pas définie.
    BOOST_ASSERT récupère les infos à passer en paramètre à boost::assertion_failed et, en bonus, optimise les branchements en disant au compilateur que l'expression évaluée est souvent vraie.
    L'utilisateur peut définir boost::assertion_failed pour faire ce qu'il veut, par exemple lancer une exception riche en informations.

  4. #4
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2009
    Messages
    4 493
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 37
    Localisation : France, Loire Atlantique (Pays de la Loire)

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

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 493
    Billets dans le blog
    1
    Par défaut
    Je réagis au premier article !

    J'ai bien aimé ! Récemment j'ai eu une discussion avec un collègue dont le sujet était justement programmation par contrat vs programmation défensive. Le débat était parti d'un code où je mettais des assertions pour vérifier que les valeurs passées en paramètres étaient bien dans les plages précisées dans la documentation de la fonction. Il s'était que je ne fasse pas un test avec un if() (pour ne rien faire ou renvoyer une erreur) à la place de assert(). Ce billet me permet de bien re-situer les tenants et aboutissants de programmation par contrat vs programmation défensive.

  5. #5
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    Le premier article affirme :
    « Le choix de remonter des exceptions, depuis le lieu de la détection de la rupture de contrat, est un choix de programmation défensive. C'est un choix que j'assimile à une déresponsabilisation des véritables responsables. »
    « Il est vrai que la programmation défensive permet d'une certaine façon de centraliser et factoriser les vérifications. Mais les vérifications ainsi centralisées ne disposent pas du contexte qui permet de remonter des erreurs correctes. Il est nécessaire d'enrichir les exceptions pauvres en les transformant au niveau du code client, et là on perd les factorisations. »

    Cependant, il y a des erreurs dans le code qui illustre les idées ci-dessus :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    double my::sqrt(double n) {
        if (n<0) throw std::domain_error("Negative number sent to sqrt");
        return std::sqrt(n);
    }
    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
    void my::process(boost::filesystem::path const& file) {
        boost::ifstream f(file);
        if (!f) throw std::runtime_error("Cannot open "+file.string());
        double d;
        while (f >> d) {
            double sq = 0;
            try {
                sq = my::sqrt(d);
            }
            catch (std::logic_error const&) {
                throw std::runtime_error(
                    "Invalid negative distance " + std::to_string(d)
                    +" at the "+std::to_string(l)
                    +"th line in distances file "+file.string());
            }
          my::memorize(sq);
        }
    }
    Erreur d'étourderie : La variable l, qui indique le numéro de ligne, n'est pas déclarée.
    Autre erreur : Par convention, std::logic_error, c'est pour une erreur de programmation, pas pour une erreur d'une donnée en entrée d'un programme (comme un fichier).
    Le genre de code critiqué, ce serait plutôt celui-ci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    double my::sqrt(double n) {
        if (n<0) throw std::runtime_error("Negative number sent to sqrt: " + std::to_string(n));
        return std::sqrt(n);
    }
    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
    void my::process(boost::filesystem::path const& file) {
        boost::ifstream f(file);
        if (!f) throw std::runtime_error("Cannot open "+file.string());
        double d;
        while (f >> d) {
            double sq = 0;
            try {
                sq = my::sqrt(d);
            }
            catch (std::runtime_error const& e) {
                throw std::runtime_error(
                    "Error in distances file "+file.string()+": "+e.what());
            }
          my::memorize(sq);
        }
    }
    Dans le code ci-dessus, on ne perd pas entièrement la factorisation, car une partie du message d'erreur est gérée par my::sqrt.

    Mais il y a bien une déresponsabilisation du code appelant. En effet, l'appelant se dira qu'il n'aura pas toujours besoin de lancer une exception de type std::runtime_error quand un nombre en entrée du programme attendu comme positif est strictement négatif car, s'il donne ce nombre en argument à my::sqrt, c'est my::sqrt qui fera le travail.

    Le problème de cette déresponsabilisation, c'est que le jour où my::sqrt recevra un argument strictement négatif suite à une vraie erreur de programmation, l'erreur ne sera pas signalée sous la forme d'erreur de programmation (par exemple avec une exception de type std::logic_error).
    Alors, le code spécifique à la gestion des erreurs de programmation (par exemple un bloc catch(std::logic_error const& e)) ne sera pas appelé.
    Et le jour où un développeur voudra que le programme détecte mieux les erreurs de programmation en redéfinissant my::sqrt en :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    double my::sqrt(double n) {
        assert(n>=0 && "sqrt can't process negative numbers");
        if (n<0) throw std::logic_error("Negative number sent to sqrt: " + std::to_string(n));
        return std::sqrt(n);
    }
    il va se heurter à des faux positifs à cause du code legacy qui partait de l'hypothèse que my::sqrt pouvait recevoir un nombre strictement négatif.

    Au quotidien, les fois où je rencontre le plus cette déresponsabilisation, ce sont les fonctions sans assert qui commencent par un if et qui ne font rien quand la condition est fausse. Comme ça, les fois où la condition est fausse à cause d'une erreur de programmation, l'erreur n'est détectée que beaucoup plus tard voire n'est même pas détectée.

    Cela dit, il est dommage que le premier article fasse parfois l'amalgame entre lancer une exception et déresponsabiliser le code appelant. Ce n'est normalement pas le cas quand l'exception lancée dérive de std::logic_error.

    Heureusement, il ne fait pas toujours cet amalgame. Je cite un passage qui ne le fait pas :
    « Si la PpC s'intéresse à l'écriture de code correct, la programmation défensive s'intéresse à l'écriture de code robuste. L'objectif premier n'est pas le même (dans un cas on essaie de repérer et éliminer les erreurs de programmation, dans l'autre on essaie de ne pas planter en cas d'erreur de programmation), de fait les deux techniques peuvent se compléter.
    [...]
    À vrai dire, on peut utiliser simultanément ces deux approches sur de mêmes contrats. En effet, il est possible de modifier la définition d'une assertion en mode Release pour lui faire lancer une exception de logique. En mode Debug elle nous aidera à contrôler les enchaînements d'opérations. »

  6. #6
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 296
    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 296
    Par défaut
    @Pyramidev.
    Merci pour le numéro de ligne que je vois et oublie régulièrement. Il faut que je m'organise pour corriger ça. Merci aussi pour l'info, pour boost. Je ne pense pas le rajouter. Je trouve mes billets déjà trop longs. Surtout le deuxième qui s'est dispersé depuis la version initiale qui se concentrait sur les assertions, et ce n'est plus trop le sujet du 3e où je montre des patterns plus ou moins heureux. J'aurai du faire une 4e billet pour présenter GSL et la mode ressuscitée des types opaques en C++.

    Je maintiens la domain_error (dérivant de logic_error) dans my::sqrt, et non une runtime_error.
    my::sqrt a un contrat: l'entrée doit être positive. Si elle est négative, c'est une erreur de logique. Même avec un contrat élargi. Ce n'est pas à my::sqrt de valider les saisies utilisateur.

    Les problèmes arrivent effectivement quand on commence à ne plus vraiment distinguer ces situations et à ne plus vraiment être capables de déterminer les responsabilités de chacun. Si on cesse de contrôler nos entrées au point où on les reçoit, on commet des erreurs de programmation, ou des fautes de style si le choix est assumé.

    Accessoirement, je ne valide pas que "Error in distance file toto.txt at line 42: error negative number sent to sqrt" soit une bonne factorisation. Déjà c'est supposé qu'un vrai code soit bien aussi simple que cela, qu'il n'y ait pas des couches intermédiaires avant l'appel à sqrt, ou pire une mémorisation des distances sous forme d'un vecteur avant de calculer nos racines carrées. Mon exemple est un truc simpliste pour le besoin de l'illustration.
    Dans les autres trucs assez simples que j'ai rencontré: des chaines lues dans un XML et passées dans boost::lexical_cast pour les convertir en nombre. On est dans les mêmes problématiques à se reposer sur une sous-couche qui renvoie une erreur au lieu de contrôler préalablement, on arrive vite avec des fiches d'anomalies ouvertes par le client sous prétexte que "Cannot execute joborder foobar: source type value could not be interpreted as target".

    Bref, je suis de plus en plus convaincu qu'une exception de logique (même si déguisé en runtime_error) est une déresponsabilisation quand elle est le seul mécanisme employé pour valider des cas invalides mais plausibles. Si maintenant, elle est là en roue de secours parce que l'on estime que l'on ne peut pas valider tous les chemins et qu'il ne faut absolument pas planter... ma foi. Je vais dire que c'est un palliatif acceptable en attendant de disposer de bon moyens pour mieux contrôler nos chemins d'exécutions -- typiquement des outils de preuve formelle.
    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...

Discussions similaires

  1. Programmation par contrat en C++
    Par bolhrak dans le forum C++
    Réponses: 11
    Dernier message: 07/09/2007, 00h12
  2. [Language]Programmation par contrat
    Par manube dans le forum Langage
    Réponses: 3
    Dernier message: 20/12/2005, 10h16
  3. [Eiffel] Programmation par contrats
    Par SkIllz2k dans le forum Autres langages
    Réponses: 1
    Dernier message: 02/05/2005, 20h05
  4. [Tests]La programmation par contrats
    Par fabien.raynaud dans le forum Test
    Réponses: 6
    Dernier message: 26/07/2004, 11h06

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