Je suis un programmeur C professionnel et un programmeur Obj-C amateur (OS X). Récemment, j'ai été tenté de développer en C ++, en raison de sa syntaxe très riche.
Jusqu'à présent, je n'ai pas beaucoup traité des exceptions avec le codage. Objective-C en a, mais la politique d'Apple est assez stricte:
Important Vous devez réserver l'utilisation d'exceptions pour la programmation ou des erreurs d'exécution inattendues telles que l'accès à la collection hors limites, les tentatives de mutation d'objets immuables, l'envoi d'un message non valide et la perte de la connexion au serveur Windows.
C ++ semble préférer utiliser les exceptions plus souvent. Par exemple, l'exemple de wikipedia sur RAII lève une exception si un fichier ne peut pas être ouvert. Objective-C return nil
avec une erreur envoyée par un paramètre de sortie. Notamment, il semble que std :: ofstream puisse être défini dans les deux sens.
Ici, sur les programmeurs, j'ai trouvé plusieurs réponses soit proclamant utiliser des exceptions au lieu de codes d'erreur, soit ne pas utiliser d'exceptions du tout . Les premiers semblent plus répandus.
Je n'ai trouvé personne faire une étude objective pour C ++. Il me semble que comme les pointeurs sont rares, 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.
Edit: Bien que pas complètement pertinent, je devrais probablement clarifier ce qui nil
est. Techniquement, c'est la même chose que NULL
, mais le fait est que c'est ok d'envoyer un message à nil
. Vous pouvez donc faire quelque chose comme
NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];
[obj retain];
même si le premier appel est revenu nil
. Et comme vous ne le faites jamais *obj
dans Obj-C, il n'y a aucun risque de déréférence de pointeur NULL.
la source
Réponses:
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 ++:
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".
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: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 invalidenullptr
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 lecatch
site analogique .Des questions
É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éesnothrow
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:
... 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.
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 à launique_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 à utiliser
shared_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 chosethrows
.la source
Voici la chose: en raison de l'historique et de la flexibilité uniques de C ++, vous pouvez trouver quelqu'un proclamant pratiquement n'importe quelle opinion sur une fonctionnalité que vous souhaitez. Cependant, en général, plus ce que vous faites ressemble à C, pire est l'idée.
L'une des raisons pour lesquelles C ++ est beaucoup plus souple en ce qui concerne les exceptions est que vous ne pouvez pas exactement
return nil
quand vous en avez envie. Il n'y a rien de tel quenil
dans la grande majorité des cas et des types.Mais voici le simple fait. Les exceptions font leur travail automatiquement. Lorsque vous lève une exception, RAII prend le relais et tout est géré par le compilateur. Lorsque vous utilisez des codes d'erreur, vous devez vous rappeler de les vérifier. Cela rend intrinsèquement plus sûr les exceptions que les codes d'erreur - ils se vérifient, pour ainsi dire. De plus, ils sont plus expressifs. Lancez une exception et vous pouvez obtenir une chaîne vous indiquant quelle est l'erreur, et peut même contenir des informations spécifiques, comme "Mauvais paramètre X qui avait la valeur Y au lieu de Z". Obtenez un code d'erreur de 0xDEADBEEF et qu'est-ce qui s'est exactement passé? J'espère bien que la documentation est complète et à jour, et même si vous obtenez "Mauvais paramètre", cela ne vous dira jamais lequel, quelle valeur il avait et quelle valeur il aurait dû avoir. Si vous les capturez par référence, elles peuvent également être polymorphes. Enfin, des exceptions peuvent être levées à des endroits où les codes d'erreur ne peuvent jamais, comme les constructeurs. Et qu'en est-il des algorithmes génériques? Comment
std::for_each
va gérer votre code d'erreur? Astuce: ce n'est pas le cas.Les exceptions sont largement supérieures aux codes d'erreur à tous égards. La vraie question est dans les exceptions contre les affirmations.
Voici le truc. Personne ne peut savoir à l'avance quelles conditions préalables votre programme a pour fonctionner, quelles sont les conditions d'échec inhabituelles, qui peuvent être vérifiées au préalable et quels sont les bogues. Cela signifie généralement que vous ne pouvez pas décider à l'avance si une défaillance donnée doit être une assertion ou une exception sans connaître la logique du programme. De plus, une opération qui peut se poursuivre lorsque l'une de ses sous-opérations échoue est l' exception , pas la règle.
À mon avis, des exceptions sont là pour être prises. Pas nécessairement immédiatement, mais finalement. Une exception est un problème dont vous vous attendez à ce que le programme puisse récupérer à un moment donné. Mais l'opération en question ne pourra jamais se remettre d'un problème qui mérite une exception.
Les échecs d'assertion sont toujours des erreurs fatales et irrécupérables. La corruption de la mémoire, ce genre de chose.
Donc, quand vous ne pouvez pas ouvrir un fichier, est-ce une affirmation ou une exception? Eh bien, dans une bibliothèque générique, il existe de nombreux scénarios où l'erreur peut être gérée, par exemple, en chargeant un fichier de configuration, vous pouvez simplement utiliser une valeur par défaut prédéfinie à la place, donc je dirais une exception.
En tant que note de bas de page, je voudrais mentionner qu'il y a quelque chose de "modèle d'objet nul" qui circule. Ce schéma est terrible . Dans dix ans, ce sera le prochain Singleton. Le nombre de cas dans lesquels vous pouvez produire un objet nul approprié est minuscule .
la source
Les exceptions ont été inventées pour une raison, qui est d'éviter que tout votre code ressemble à ceci:
Au lieu de cela, vous obtenez quelque chose qui ressemble davantage à ce qui suit, avec un gestionnaire d'exceptions en place
main
ou autrement situé de manière pratique:Certaines personnes revendiquent des avantages en termes de performances à l'approche sans exception, mais à mon avis, les problèmes de lisibilité l'emportent sur les gains de performances mineurs, en particulier lorsque vous incluez le temps d'exécution pour tout le passe-partout que
if (!success)
vous devez saupoudrer partout, ou le risque de plus difficile à déboguer les défauts de segmentation si vous ne le faites pas. t l'inclure, et compte tenu des chances d'exceptions sont relativement rares.Je ne sais pas pourquoi Apple décourage l'utilisation d'exceptions. S'ils essaient d'éviter de propager des exceptions non gérées, tout ce que vous accomplissez vraiment, ce sont des personnes utilisant des pointeurs nuls pour indiquer des exceptions à la place, donc une erreur de programmeur entraîne une exception de pointeur nul au lieu d'un fichier beaucoup plus utile, une exception introuvable ou quoi que ce soit.
la source
Dans le post que vous référencez (exceptions vs codes d'erreur), je pense qu'il y a une discussion subtilement différente en cours. La question semble être de savoir si vous avez une liste globale de codes d'erreur #define, probablement complète avec des noms comme ERR001_FILE_NOT_WRITEABLE (ou en abrégé, si vous n'avez pas de chance). Et, je pense que le point principal de ce fil est que si vous programmez dans un langage polymporphique, en utilisant des instances d'objet, une telle construction procédurale n'est pas nécessaire. Les exceptions peuvent exprimer quelle erreur se produit simplement en raison de leur type, et elles peuvent encapsuler des informations telles que le message à imprimer (et d'autres choses également). Donc, je considérerais cette conversation comme une question de savoir si vous devez programmer de manière procédurale dans un langage orienté objet ou non.
Mais, la conversation sur quand et dans quelle mesure s'appuyer sur des exceptions pour gérer les situations qui surviennent dans le code est différente. Lorsque des exceptions sont levées et interceptées, vous introduisez un paradigme de flux de contrôle complètement différent de la pile d'appels traditionnelle. La levée d'exception est fondamentalement une instruction goto qui vous arrache de votre pile d'appels et vous envoie vers un emplacement indéterminé (partout où votre code client décide de gérer votre exception). Cela rend la logique d'exception très difficile à raisonner .
Et donc, il y a un sentiment comme celui exprimé par jheriko que ceux-ci devraient être minimisés. Autrement dit, disons que vous lisez un fichier qui peut ou non exister sur le disque. Jheriko et Apple et ceux qui pensent comme eux (moi y compris) diraient que lever une exception n'est pas approprié - l'absence de ce fichier n'est pas exceptionnelle , c'est prévu. Les exceptions ne doivent pas remplacer le flux de contrôle normal. Il est tout aussi simple que votre méthode ReadFile () retourne, disons, un booléen, et que le code client voie à la valeur de retour false que le fichier n'a pas été trouvé. Le code client peut alors dire que le fichier utilisateur n'a pas été trouvé, ou gérer tranquillement ce cas, ou tout ce qu'il veut faire.
Lorsque vous jetez une exception, vous imposez un fardeau à vos clients. Vous leur donnez du code qui, parfois, les arrachera à la pile d'appels normaux et les forcera à écrire du code supplémentaire pour empêcher leur application de se bloquer. Un fardeau aussi puissant et discordant ne devrait leur être imposé que si cela est absolument nécessaire au moment de l'exécution, ou dans l'intérêt de la philosophie du «premier échec». Donc, dans le premier cas, vous pouvez lever des exceptions si le but de votre application est de surveiller le fonctionnement du réseau et que quelqu'un débranche le câble réseau (vous signalez une panne catastrophique). Dans ce dernier cas, vous pouvez lever une exception si votre méthode attend un paramètre non nul et que vous êtes remis à zéro (vous signalez que vous êtes utilisé de manière inappropriée).
Quant à ce qu'il faut faire au lieu d'exceptions dans le monde orienté objet, vous avez des options au-delà de la construction du code d'erreur procédurale. Un qui me vient immédiatement à l'esprit est que vous pouvez créer des objets appelés OperationResult ou quelque chose de ce genre. Une méthode peut renvoyer un OperationResult et cet objet peut avoir des informations sur le succès ou l'échec de l'opération, et sur les autres informations que vous souhaitez lui donner (un message d'état, par exemple, mais aussi, plus subtilement, il peut encapsuler des stratégies de récupération d'erreur ). Renvoyer cela au lieu de lever une exception permet le polymorphisme, préserve le flux de contrôle et rend le débogage beaucoup plus simple.
la source
Il y a quelques beaux chapitres dans Clean Code qui parlent de ce sujet même - moins le point de vue d'Apple.
L'idée de base est que vous ne renvoyez jamais rien de vos fonctions, car cela:
L'autre idée qui l'accompagne est que vous utilisez des exceptions car cela aide à réduire davantage l'encombrement de votre code, et plutôt que d'avoir à créer une série de codes d'erreur qui deviennent finalement difficiles à gérer et à maintenir (en particulier sur plusieurs modules et plates-formes) et sont difficiles à déchiffrer pour vos clients, vous créez plutôt des messages significatifs qui identifient la nature exacte du problème, simplifiez le débogage et pouvez réduire votre code de gestion des erreurs à quelque chose d'aussi simple qu'une
try..catch
instruction de base .À l'époque de mon développement COM, j'avais un projet où chaque échec soulevait une exception, et chaque exception devait utiliser un identifiant de code d'erreur unique basé sur l'extension des exceptions COM Windows standard. C'était un maintien d'un projet précédent où des codes d'erreur avaient été précédemment utilisés, mais la société voulait aller tout orientée objet et sauter dans le train de la COM sans changer la façon dont ils faisaient trop les choses. Il ne leur a fallu que 4 mois pour manquer de numéros de code d'erreur et c'est à moi de refactoriser de grandes portions de code pour utiliser des exceptions à la place. Mieux vaut vous épargner l'effort à l'avance et utiliser simplement les exceptions judicieusement.
la source