C ++ semble préférer utiliser les exceptions plus souvent.
Je suggérerais en fait moins qu'Objective-C à certains égards parce que la bibliothèque standard C ++ ne déclencherait généralement pas d'erreurs de programmation comme l'accès hors limites d'une séquence à accès aléatoire dans sa forme de conception de cas la plus courante (dans operator[]
, c'est- à -dire) ou essayer de déréférencer un itérateur invalide. Le langage ne permet pas d'accéder à un tableau en dehors des limites, ni de déréférencer un pointeur nul, ou quoi que ce soit de ce genre.
Prendre les erreurs de programmation en grande partie hors de l'équation de gestion des exceptions enlève en fait une très grande catégorie d'erreurs auxquelles d'autres langues répondent souvent throwing
. Le C ++ a tendance à assert
(qui n'est pas compilé dans les versions de publication / production, seulement les versions de débogage) ou simplement à se briser (souvent en panne) dans de tels cas, probablement en partie parce que le langage ne veut pas imposer le coût de ces vérifications d'exécution comme cela serait nécessaire pour détecter de telles erreurs de programmation à moins que le programmeur ne veuille spécifiquement payer les coûts en écrivant du code qui effectue lui-même de telles vérifications.
Sutter encourage même à éviter les exceptions dans de tels cas dans les normes de codage C ++:
Le principal inconvénient de l'utilisation d'une exception pour signaler une erreur de programmation est que vous ne voulez pas vraiment que le déroulement de la pile se produise lorsque vous souhaitez que le débogueur se lance sur la ligne exacte où la violation a été détectée, avec l'état de la ligne intact. En résumé: vous savez qu'il peut y avoir des erreurs (voir les points 69 à 75). Pour tout le reste qui ne devrait pas, et c'est la faute du programmeur si c'est le cas, il y en a assert
.
Cette règle n'est pas nécessairement figée. Dans certains cas plus critiques, il peut être préférable d'utiliser, par exemple, des wrappers et une norme de codage qui enregistre uniformément les erreurs de programmation et throw
en présence d'erreurs de programmation, comme essayer de déférer quelque chose d'invalide ou d'y accéder hors des limites, car il pourrait être trop coûteux de ne pas récupérer dans ces cas si le logiciel a une chance. Mais dans l'ensemble, l'utilisation la plus courante du langage a tendance à ne pas se heurter à des erreurs de programmation.
Exceptions externes
Là où je vois des exceptions encouragées le plus souvent en C ++ (selon le comité standard, par exemple), c'est pour les "exceptions externes", comme dans un résultat inattendu dans une source externe en dehors du programme. Un exemple ne parvient pas à allouer de la mémoire. Un autre ne parvient pas à ouvrir un fichier critique requis pour l'exécution du logiciel. Un autre ne parvient pas à se connecter à un serveur requis. Un autre est un utilisateur bloquant un bouton d'abandon pour annuler une opération dont le chemin d'exécution de cas commun s'attend à réussir en l'absence de cette interruption externe. Toutes ces choses échappent au contrôle du logiciel immédiat et des programmeurs qui l'ont écrit. Ce sont des résultats inattendus de sources externes qui empêchent l'opération (qui devrait vraiment être considérée comme une transaction indivisible dans mon livre *) de réussir.
Transactions
J'encourage souvent à considérer un try
bloc comme une "transaction" car les transactions devraient réussir dans leur ensemble ou échouer dans leur ensemble. Si nous essayons de faire quelque chose et que cela échoue à mi-chemin, alors tous les effets secondaires / mutations apportés à l'état du programme doivent généralement être annulés pour remettre le système dans un état valide comme si la transaction n'avait jamais été exécutée, tout comme un SGBDR qui ne parvient pas à traiter une requête à mi-parcours ne doit pas compromettre l'intégrité de la base de données. Si vous mutez l'état du programme directement dans ladite transaction, vous devez le "réactiver" s'il rencontre une erreur (et ici, les gardes de portée peuvent être utiles avec RAII).
L'alternative beaucoup plus simple est de ne pas muter l'état du programme d'origine; vous pouvez muter une copie de celui-ci puis, s'il réussit, échanger la copie avec l'original (en vous assurant que l'échange ne peut pas être lancé). S'il échoue, jetez la copie. Cela s'applique également même si vous n'utilisez pas d'exceptions pour la gestion des erreurs en général. Un état d'esprit "transactionnel" est la clé d'une récupération correcte si des mutations d'état du programme se sont produites avant de rencontrer une erreur. Il réussit dans son ensemble ou échoue dans son ensemble. Il ne parvient pas à moitié à faire ses mutations.
C'est bizarrement l'un des sujets les moins fréquemment discutés lorsque je vois des programmeurs demander comment faire correctement la gestion des erreurs ou des exceptions, mais il est le plus difficile à obtenir dans n'importe quel logiciel qui veut muter directement l'état du programme dans de nombreux ses opérations. La pureté et l'immuabilité peuvent aider ici à atteindre une sécurité d'exception tout autant qu'elles aident à la sécurité des fils, car une mutation / un effet secondaire externe qui ne se produit pas n'a pas besoin d'être annulé.
Performance
Un autre facteur déterminant dans l'utilisation ou non des exceptions est la performance, et je ne veux pas dire d'une manière obsessionnelle, penny-pinching, contre-productive. De nombreux compilateurs C ++ implémentent ce que l'on appelle le "traitement d'exception à coût nul".
Il n'offre aucun temps d'exécution pour une exécution sans erreur, ce qui dépasse même celui de la gestion des erreurs de valeur de retour C. En guise de compromis, la propagation d'une exception a une surcharge importante.
Selon ce que j'ai lu à ce sujet, cela rend vos chemins d'exécution de cas courants ne nécessitent pas de surcharge (pas même la surcharge qui accompagne normalement la gestion et la propagation du code d'erreur de style C), en échange de fausser fortement les coûts vers les chemins exceptionnels ( ce qui signifie throwing
est maintenant plus cher que jamais).
"Cher" est un peu difficile à quantifier mais, pour commencer, vous ne voulez probablement pas lancer un million de fois dans une boucle étroite. Ce type de conception suppose que les exceptions ne se produisent pas toujours à gauche et à droite.
Non-erreurs
Et ce point de performance m'amène à des non-erreurs, ce qui est étonnamment flou si l'on regarde toutes sortes d'autres langages. Mais je dirais, étant donné la conception EH à coût nul mentionnée ci-dessus, que vous ne voulez certainement pas throw
en réponse à une clé qui ne se trouve pas dans un ensemble. Parce que non seulement c'est sans doute une non-erreur (la personne recherchant la clé peut avoir construit l'ensemble et s'attendre à rechercher des clés qui n'existent pas toujours), mais cela coûterait énormément cher dans ce contexte.
Par exemple, une fonction d'intersection d'ensemble peut vouloir parcourir deux ensembles et rechercher les clés qu'ils ont en commun. Si threw
vous ne parvenez pas à trouver une clé , vous seriez en boucle et pourriez rencontrer des exceptions dans la moitié ou plus des itérations:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Cet exemple ci-dessus est absolument ridicule et exagéré, mais j'ai vu, dans le code de production, certaines personnes venant d'autres langages utiliser des exceptions en C ++ un peu comme ça, et je pense que c'est une déclaration raisonnablement pratique que ce n'est pas une utilisation appropriée des exceptions que ce soit en C ++. Un autre indice ci-dessus est que vous remarquerez que le catch
bloc n'a absolument rien à faire et qu'il est juste écrit pour ignorer de force de telles exceptions, et c'est généralement un indice (bien qu'il ne soit pas un garant) que les exceptions ne sont probablement pas utilisées de manière très appropriée en C ++.
Pour ces types de cas, un certain type de valeur de retour indiquant l'échec (que ce soit de retourner false
à un itérateur invalide nullptr
ou à tout ce qui a du sens dans le contexte) est généralement beaucoup plus approprié, et aussi souvent plus pratique et productif car un type sans erreur de case n'appelle généralement pas de processus de déroulement de pile pour atteindre le catch
site analogique .
Des questions
Je devrais utiliser des indicateurs d'erreur internes si je choisis d'éviter les exceptions. Sera-ce trop difficile à gérer, ou cela fonctionnera-t-il peut-être encore mieux que les exceptions? Une comparaison des deux cas serait la meilleure réponse.
Éviter purement et simplement les exceptions en C ++ me semble extrêmement contre-productif, à moins que vous ne travailliez dans un système embarqué ou un type particulier de cas qui interdit leur utilisation (auquel cas vous devrez également faire tout votre possible pour éviter tout bibliothèque et des fonctionnalités linguistiques qui, autrement throw
, seraient strictement utilisées nothrow
new
).
Si vous devez absolument éviter les exceptions pour une raison quelconque (ex: travailler à travers les limites de l'API C d'un module dont vous exportez l'API C), beaucoup pourraient être en désaccord avec moi, mais je suggérerais en fait d'utiliser un gestionnaire / état d'erreur global comme OpenGL avec glGetError()
. Vous pouvez lui faire utiliser le stockage local de threads pour avoir un statut d'erreur unique par thread.
Ma raison en est que je n'ai pas l'habitude de voir des équipes dans des environnements de production vérifier soigneusement toutes les erreurs possibles, malheureusement, lorsque les codes d'erreur sont renvoyés. S'ils étaient approfondis, certaines API C peuvent rencontrer une erreur avec à peu près chaque appel d'API C, et une vérification approfondie nécessiterait quelque chose comme:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... avec presque chaque ligne de code invoquant l'API nécessitant de telles vérifications. Pourtant, je n'ai pas eu la chance de travailler avec des équipes aussi approfondies. Ils ignorent souvent de telles erreurs la moitié, parfois même la plupart du temps. C'est le plus grand attrait pour moi des exceptions. Si nous encapsulons cette API et la rendons uniforme throw
lors de la rencontre d'une erreur, l'exception ne peut pas être ignorée , et à mon avis, et par expérience, c'est là que réside la supériorité des exceptions.
Mais si les exceptions ne peuvent pas être utilisées, le statut d'erreur global par thread a au moins l'avantage (énorme par rapport au renvoi de codes d'erreur) qu'il pourrait avoir une chance de détecter une ancienne erreur un peu plus tard que lorsqu'il s'est produit dans une base de code bâclée au lieu de la manquer complètement et de nous laisser complètement inconscients de ce qui s'est passé. L'erreur peut s'être produite quelques lignes auparavant, ou lors d'un appel de fonction précédent, mais à condition que le logiciel ne soit pas encore tombé en panne, nous pourrons peut-être commencer à revenir en arrière et déterminer où et pourquoi cela s'est produit.
Il me semble que comme les pointeurs sont rares, je devrais utiliser des indicateurs d'erreur internes si je choisis d'éviter les exceptions.
Je ne dirais pas nécessairement que les pointeurs sont rares. Il existe même des méthodes désormais en C ++ 11 et ultérieur pour accéder aux pointeurs de données sous-jacents des conteneurs, et un nouveau nullptr
mot-clé. Il est généralement considéré comme imprudent d'utiliser des pointeurs bruts pour posséder / gérer la mémoire si vous pouvez utiliser quelque chose comme à la unique_ptr
place étant donné à quel point il est essentiel d'être conforme à RAII en présence d'exceptions. Mais les pointeurs bruts qui ne possèdent pas / ne gèrent pas la mémoire ne sont pas nécessairement considérés comme si mauvais (même par des gens comme Sutter et Stroustrup) et parfois très pratiques comme moyen de pointer des choses (avec des indices qui pointent vers des choses).
Ils ne sont sans doute pas moins sûrs que les itérateurs de conteneurs standard (au moins dans la version, en l'absence d'itérateurs vérifiés) qui ne détecteront pas si vous essayez de les déréférencer après leur invalidation. Le C ++ est encore sans vergogne un peu un langage dangereux, je dirais, à moins que votre utilisation spécifique de celui-ci veuille tout envelopper et cacher même les pointeurs bruts non propriétaires. Il est presque essentiel, à quelques exceptions près, que les ressources soient conformes à RAII (qui ne coûte généralement aucun frais d'exécution), mais à part cela, il n'essaie pas nécessairement d'être le langage le plus sûr à utiliser pour éviter les coûts qu'un développeur ne souhaite pas explicitement. échanger contre autre chose. L'utilisation recommandée n'essaie pas de vous protéger contre des choses comme les pointeurs pendants et les itérateurs invalides, pour ainsi dire (sinon nous serions encouragés à utilisershared_ptr
partout, ce à quoi Stroustrup s’oppose avec véhémence). Il essaie de vous protéger contre l'échec de libérer / libérer / détruire / déverrouiller / nettoyer correctement une ressource quand quelque chose throws
.