Partage

Question sur les exceptions

Sujet résolu
9 avril 2016 à 11:52:15

Bonjour à tous j'aurai une questions sur les exceptions qui pourrait se résumer quand et comment doit on les utiliser ?

Je m'explique

Imaginons que mon code effectue une division.

Dois je soulever une exception pour le cas ou il y aurait une division par 0 ou au contraire dois je plutôt mettre en place des barrières pour m'assurer que tous simplement çà n'arrive jamais ?

En claire doit on lancer des exceptions pour ce qui pourrait arriver de manière exceptionnelles et des choses sur lesquelles on a pas la main (erreur de mémoire ,verrouillage d'un fichier ou autre).

Ou au contraire je dois en mettre le plus possible partout et ne pas m'embêter à tenter de gérer les erreurs prévisible et qui pourrait arriver fréquemment (une saisi dans un format non valide, etc..)

9 avril 2016 à 13:06:21

Salut,
lancer une exception est une étape coûteuse en performances à cause du tas d'instructions assembleur généré après. Donc on les utilises dans des scénarios "exceptionnels", dont ni le développeur, ni l'utilisateur (de la fonction/classe créé par le développeur) ne sont responsables. Un exemple: Si une connexion à un serveur ne fonctionne pas, il semble logique de lancer une exception, plutôt que de claquer une assertion, qui aura tué le programme sans aucune chance de le dépanner (même si sans les try-catch, les exceptions vont aussi faire planter le programme). Pire encore, en période de production, imagine que le serveur ne réponds pas, et que comme l'assertion n'a plus aucun effet, que se passera-t-il ? Le programme va faire "comme si le serveur a répondu" et finira par faire n'importe quoi, voir planter dans le meilleur des cas. Et l'utilisateur de ton programme ne veut pas que ton programme plante, il veut qu'il fonctionne. Donc, dans ce cas, exceptions.

On claque une assertion pour gérer les erreurs commises par l'utilisateur de la fonction/classe. Il te donne de mauvais paramètres, la fonction f() ne peut plus travailler correctement et le signale en plantant le programme. Théoriquement, tu n'a plus aucune protection en période de production, mais en période de développement, elle servent à "traquer" les erreurs pour t'éviter de tomber sur un comportement indéterminé, qui à certains moments fonctionnera, et à d'autre, plantera. Dans ta fonction division, le mieux est de claquer une assertion. C'est l'utilisateur de la fonction qui a donné de mauvais paramètres la fonction, ce n'est pas un problème d'environnement.

PS: C++11 est venu avec le mot-clé "static_assert()". Elle permet d'avoir une erreur, exactement comme assert(), mais à la compilation alors que assert() est évalué à l’exécution. N'empêche que static_assert() à des limites: les paramètres qui lui sont données doivent être évaluable à la compilation (comme les fonctions et variables constexpr, les classes de <type_traits>, les valeurs numériques comme 3 ...). On ne peut pas les utiliser avec des variables, fonctions non-constexpr ... 

9 avril 2016 à 13:31:15

Salut

Merci pour cette réponse très complète.

J'avoue ne pas forcément avoir tout compris car je n'ai pas encore vu les assert.

Je vais aller lire un peu plus la donc sur ce sujet

Question tous de même si je comprend bien si je reprend l'exemple d'une focntion division

je met une assert qui vérifie qu'on ne divise pas par 0 et si l'utilisateur indique 0 çà va planter le programme

Quelle différence avec le laisser faire ? Si je ne gère pas le cas le programme va planter aussi non ?

Ne serait il pas mieux de renvoyer un message indiquant l'erreur afin que l'utilisateur la corrige ?

9 avril 2016 à 13:57:39

Sans l'assertion, une division par 0 est un comportement indéterminé. Ce sera dans le meilleur des cas un plantage, dans le pire des cas il y aura un fonctionnement normal. Un plantage du programme par UB (Underfined Behavior = comportement indéterminé) peut avoir lieu, qui sait, dès la division, peut-être beaucoup plus loin (2 lignes de code plus loin ? 30 ? 98 ?) car la division aurait qui sait donné n'importe quoi.
(note: il y a comportement indéterminé seulement avec les entiers, les floats, doubles ... divisés par 0.0 donne "Nan") 

Avec l'assertion, tu es clairement prévenu:

  • Du fichier où se trouve l'assertion échoué
  • Du numéro de la ligne où se trouve l'assertion échoué
  • La condition échouée (avec ou pas un message descriptif) 
  • etc ...

Tu peux t'aider à corriger un UB avec ton débogueur si ton programme plante (au moins, tu pourrais t'en sortir), ou avec des outils comme Valgrind pour les problèmes de mémoires.

Mais généralement, le débogage est assez énervant (temps pour localiser l'erreur + temps pour corriger l'erreur), et que un simple fait de mettre une assertion peut te libérer la tâche de localiser l'erreur (bon, pas toujours, mais souvent, ce n'est plus aussi compliqué).

En plus, les assertions déclenche ce qu'on appelle un "coredump", une espèce de "sauvegarde" de la mémoire au moment du plantage, et permet de savoir un peu dans quel état était le programme lorsque le problème est survenu.

Néanmoins, le coredump ne peut pas répondre à toutes tes questions et les fichiers de log restent la meilleur source d'information pour voir les événements qui ont pu conduire le plantage.

(Merci au livre de @koala01 :)

-
Edité par Ilearn32 9 avril 2016 à 14:03:52

9 avril 2016 à 14:38:58

Salut.

Pour la division par zéro, pose toi la question suivante : d'où vient le 0 ?

  • Si il vient d'un calcul interne de ton programme qui ne devrait pas retourner 0, c'est une erreur de programmation. Ce cas ne relève pas d'une exception ;
  • Si il vient de l'extérieur du programme (interface utilisateur, réseau, fichier, etc.), tu n'as pas le contrôle dessus. Il faut donc tester ces cas et prendre l'action appropriée (exception, message d'erreur, message de log, terminaison propre du programme, etc.). Mais dans tous les cas, une entrée de données sur laquelle tu n'as pas le contrôle ne doit jamais faire planter ton programme. Ne jamais faire confiance à l'utilisateur, et prévoir toutes les possibilités d'entrées.
Pony : Un langage à acteurs sûr et performant
9 avril 2016 à 15:05:59

ok je pense y voire plus clair désormais.

je vais pouvoir me lancer dans mon premier petit projet perso :)

Un grand merci

9 avril 2016 à 15:11:08

Pour bien comprendre la gestion des erreurs, je pense qu'il faut regarder aussi le contexte d'appel de la fonction. Par exemple, en restant sur ta fonction division.

/// \pre aucune
/// \post entier entre -10 et 10
int read_from_file() {
    ... osef
}

/// \pre aucune
/// \post tableau d'entiers entre 1 et 10
vector<int> generate_array() {
    ... osef
}

/// \pre ???
/// \post retourne a/b
int division(int a, int b) {
    ???
}

Les 3 slashs indiquent une documentation doxygen, \pre indique une précondition, et \post indique une postcondition. Un précondition est une condition que doit respecter celui qui appelle une fonction, sinon le comportement de la fonction n'est pas garantie. La postcondition est quelque chose que garantie celui qui a écrit la fonction, si elle est correctement appelée. (Ce sont des notions de programmation par contrat).

Commençons par une approche via retour d'erreur. Par exemple, on écrit :

/// \pre aucune
/// \post return a/b ou false si division par 0
std::pair<int, bool> division(int a, int b) {
    if (b == 0) 
        return std::make_pair(0, false);
    else
        return std::make_pair(a/b, true);
}

C'est ce que fait par exemple std::map::insert. Plusieurs problèmes :

Autre solution, pour corriger le second point : avec des exceptions.
/// \pre aucune
/// \post return a/b, peut lancer une exception
int division(int a, int b) {
    if (b == 0) 
        throw "division par zéro";
    return std::make_pair(a/b, true);
}

C'est ce que fait par exemple std::vector::at. Le code est un peu plus simple et celui qui appelle cette fonction ne peut pas ignorer l'erreur. Par contre, on a toujours le surcoût inutile et c'est de la programmation défensive (bof bof).

On peut donc remarquer un point important : celui qui écrit la fonction division ne sait pas si elle va être appelée avec des arguments invalides (par exemple après read_from_file) ou pas (après generate_array). Si la vérification de la division par 0 à un coût (par exemple si on met un if), c'est un coût potentiellement inutile.

Pourquoi on n'aime pas trop la programmation défensive ? Parce que vérifier systématiquement ce que fait celui qui appelle la fonction, c'est le prendre pour un idiot. Et comme cela a un coût et que les devs C++ ne sont pas idiots (darwinisme), c'est une approche bof bof.

Pour supprimer le surcoût, un seul moyen : on ne vérifie pas la division par 0, on indique explicitement dans les préconditions que le diviseur ne doit pas être nul. C'est de la responsabilité de celui qui appelle la fonction de respecter les précondtions. S'il ne le fait pas, c'est une erreur de programmation.

C'est là qu'intervient assert (et static_assert). On veut ne pas avoir de surcoût pour l'utilisateur final, mais quand même vérifier un peu les erreurs de programmation (personne n'est infaillible). assert est donc active en debug (pour les développeurs uniquement donc) et inactif en release (pour les utilisateurs finaux). Cela va permettre d'écrire des tests unitaires, pour vérifier les préconditions en debug.

La responsabilité de vérifer la division par 0 est transférée au code appelant.

/// \pre b!= 0
/// \post retourne a/b
int division(int a, int b) {
    assert(b != 0);
    return a/b;
}

{
    int a = read_from_file();
    int b = read_from_file();
    if (b != 0)
        std::cout << division(a,b) << std::endl;
    else 
        std::cout << "Valeurs invalides" << std::endl;
}

{
    auto v = generate_array();
    std::transform(begin(v), end(v), begin(v), division);
}

(C'est ce que fait par exemple std::vector::operator[]).

Donc au final, une stratégie correcte sera par exemple :

  • pour une donnée provenant des utilisateurs (cin, fichier, réseau, etc), on vérifie les valeurs avant d'appeler une fonction, avec des if.
  • pour une situation qui ne devrait pas arriver, mais qui arrive exceptionnellement (par exemple un fichier qui devient inaccessible en plein milieu de la lecteur ou la perte d'une connexion réseau), on lance une exception
  • pour créer une fonction : on exprime les préconditions dans la doc et avec des assert dans le code. Et les post conditions sont vérifiées avec des tests unitaires (ie est-ce que la fonction fait bien ce qu'on attend d'elle)

Ce sont des règles de base, assez simples à retenir et à appliquer je pense.

(EDIT : trop long pour écrire mon message, grilled par plusieurs autres réponses. Pas grave)

-
Edité par gbdivers 9 avril 2016 à 15:15:04

Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.
9 avril 2016 à 15:22:00

@gbdiver

ouaou c'est on ne peut plus limpide je trouve :)

Limite cette réponse mériterait d'être un section de ton cours tellement elle est claire et didactique 

un grand merci aussi.

9 avril 2016 à 18:41:13

Pour aller plus loin, j'aime bien les billets de blog sur la prog par contrat écrits par lmghs (merci à lui ;) ).

Mehdidou99 - Plus on apprend, et euh... plus on apprend. | Ne lisez pas le cours de C++ d'OpenClassrooms ! Il est de mauvaise qualité. Essayez plutôt celui-là. Jeux (3D) en C++ ? Unreal Engine 4, c'est bien, mangez-en !

Question sur les exceptions

× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
  • Editeur
  • Markdown