Try-catch ou ifs pour la gestion des erreurs en C ++

30

Les exceptions sont-elles largement utilisées dans la conception du moteur de jeu ou il est préférable d'utiliser des instructions if pures? Par exemple avec des exceptions:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

et avec ifs:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

J'ai entendu dire que les exceptions sont un peu plus lentes que les ifs et je ne devrais pas mélanger les exceptions avec la méthode if-checking. Cependant, à quelques exceptions près, j'ai un code plus lisible qu'avec des tonnes d'if après chaque méthode initialize () ou quelque chose de similaire, bien qu'ils soient parfois trop lourds pour une seule méthode à mon avis. Sont-ils acceptables pour le développement de jeux ou préfèrent-ils s'en tenir aux simples ifs?

tobi
la source
Beaucoup de gens prétendent que Boost est mauvais pour le développement de jeux, mais je vous recommande de regarder ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) votre exemple ifs un peu plus agréable
ManicQin
Il y a une belle discussion sur la gestion des erreurs par Andrei Alexandrescu, j'ai utilisé une approche similaire à la sienne sans exception.
Thelvyn

Réponses:

48

Réponse courte: lisez Sensible Error Handling 1 , Sensible Error Handling 2 et Sensible Error Handling 3 de Niklas Frykholm. En fait, lisez tous les articles de ce blog pendant que vous y êtes. Je ne dirai pas que je suis d'accord avec tout, mais la plupart sont en or.

N'utilisez pas d'exceptions. Il y a une pléthore de raisons. Je vais énumérer les principaux.

Ils peuvent en effet être plus lents, bien que cela soit un peu minimisé sur les nouveaux compilateurs. Certains compilateurs prennent en charge les «exceptions de surcharge zéro» pour les chemins de code qui ne déclenchent pas réellement d'exception (bien que ce soit un peu faux, car il existe encore des données supplémentaires que la gestion des exceptions nécessite, gonflant la taille de votre exécutable / dll). Le résultat final est cependant que oui, l'utilisation des exceptions est plus lente, et dans tous les chemins de code critiques pour les performances , vous devez absolument les éviter. Selon votre compilateur, les avoir activés peut ajouter des frais généraux. Ils gonflent toujours toujours la taille du code, de manière assez significative dans de nombreux cas, ce qui peut gravement affecter les performances du matériel d'aujourd'hui.

Les exceptions rendent le code beaucoup plus fragile. Il y a un infâme graphique (que je ne peux malheureusement pas trouver en ce moment) qui montre simplement sur un graphique la difficulté d'écrire du code sans exception contre du code sans exception, et le premier est une barre beaucoup plus grande. Il y a simplement beaucoup de petits accrochages avec des exceptions, et de nombreuses façons d'écrire du code qui semble sûr d'exception mais qui ne l'est vraiment pas. Même tout le comité C ++ 11 s'est trompé sur celui-ci et a oublié d'ajouter des fonctions d'aide importantes pour utiliser correctement std :: unique_ptr, et même avec ces fonctions d'aide, il faut plus de frappe pour les utiliser qu'improbable et la plupart des programmeurs ont gagné '' t même réaliser ce qui ne va pas s'ils ne le font pas.

Plus spécifiquement pour l'industrie des jeux, les compilateurs / runtimes fournis par certaines consoles ne prennent pas en charge les exceptions, voire ne les supportent pas du tout. Si votre code utilise des exceptions, pourriez-vous encore de nos jours devoir réécrire des parties de votre code pour le porter sur de nouvelles plates-formes. (Je ne sais pas si cela a changé au cours des 7 années qui ont suivi la sortie desdites consoles; nous n'utilisons tout simplement pas d'exceptions, les désactiver même dans les paramètres du compilateur, donc je ne sais pas si quelqu'un à qui j'ai parlé a même vérifié récemment.)

La ligne de pensée générale est assez claire: utilisez des exceptions pour des circonstances exceptionnelles . Utilisez-les lorsque votre programme entre dans un "Je n'ai aucune idée de quoi faire, peut-être avec un peu de chance, quelqu'un d'autre le fera, donc je lèverai une exception et verrai ce qui se passe." Utilisez-les lorsqu'aucune autre option n'a de sens. Utilisez-les lorsque vous ne vous souciez pas si vous perdez accidentellement un peu de mémoire ou si vous ne nettoyez pas une ressource parce que vous avez gâché l'utilisation de poignées intelligentes appropriées. Dans tous les autres cas, ne les utilisez pas.

Concernant le code comme votre exemple, vous avez plusieurs autres façons de résoudre le problème. L'un des plus robustes - mais pas nécessairement le plus idéal dans votre exemple simple - est de se pencher sur les types d'erreur monadiques. Autrement dit, createText () peut renvoyer un type de poignée personnalisé plutôt qu'un entier. Ce type de descripteur possède des accesseurs pour mettre à jour ou contrôler le texte. Si le descripteur est placé dans un état d'erreur (car createText () a échoué), les appels ultérieurs au descripteur échouent simplement en silence. Vous pouvez également interroger le descripteur pour voir s'il a commis une erreur et, dans l'affirmative, quelle était la dernière erreur. Cette approche a plus de frais généraux que d'autres options, mais elle est assez solide. Utilisez-le dans les cas où vous devez effectuer une longue chaîne d'opérations dans un contexte où une seule opération peut échouer en production, mais où vous ne pouvez pas / ne pouvez pas / gagnez '

Une alternative à l'implémentation de la gestion des erreurs monadique consiste à, plutôt que d'utiliser des objets de poignée personnalisés, faire en sorte que les méthodes de l'objet de contexte traitent avec grâce les ID de poignée non valides. Par exemple, si createText () renvoie -1 en cas d'échec, tous les autres appels à m_statistics qui prennent l'un de ces descripteurs doivent se terminer correctement si -1 est transmis.

Vous pouvez également placer l'erreur d'impression dans la fonction qui échoue réellement. Dans votre exemple, createText () contient probablement beaucoup plus d'informations sur ce qui ne va pas, il pourra donc vider une erreur plus significative dans le journal. Il y a peu d'avantages dans ce cas à pousser la gestion des erreurs / l'impression vers les appelants. Faites-le lorsque les appelants ont besoin de personnaliser la gestion (ou d'utiliser l'injection de dépendance). Notez qu'avoir une console en jeu qui peut apparaître chaque fois qu'une erreur est enregistrée est une bonne idée et aide ici aussi.

La meilleure option (déjà présentée dans la série d'articles liés ci-dessus) pour les appels que vous ne prévoyez pas d'échouer dans un environnement sain - comme le simple fait de créer des taches de texte pour un système de statistiques - est d'avoir simplement la fonction qui a échoué (createText dans votre exemple) abandonner. Vous pouvez être raisonnablement sûr que createText () n'échouera pas en production à moins que quelque chose ne soit totalement éliminé (par exemple, l'utilisateur a supprimé les fichiers de données de police ou, pour une raison quelconque, n'a que 256 Mo de mémoire, etc.). Dans bon nombre de ces cas, il n'y a même pas de bonne chose à faire en cas d'échec. Mémoire insuffisante? Vous pourriez même ne pas être en mesure de faire une allocation nécessaire pour créer un joli panneau GUI pour montrer à l'utilisateur l'erreur OOM. Polices manquantes? Rend difficile l'affichage des erreurs à l'utilisateur. Tout ce qui ne va pas,

Le plantage est tout à fait correct tant que vous (a) enregistrez l'erreur dans un fichier journal et (b) ne le faites que sur des erreurs qui ne sont pas causées par des actions utilisateur régulières.

Je ne dirais pas la même chose du tout pour de nombreuses applications de serveur, où la disponibilité est critique et la surveillance des chiens de garde n'est pas suffisante, mais c'est assez différent du développement du client de jeu . Je vous déconseillerais également fortement d'utiliser C / C ++ car les fonctionnalités de gestion des exceptions d'autres langages ne tendent pas à mordre comme C ++ car ce sont des environnements gérés et n'ont pas tous les problèmes de sécurité des exceptions que C ++ a. Tout problème de performances est également atténué car les serveurs ont tendance à se concentrer davantage sur le parallélisme et le débit que sur les garanties de latence minimale comme les clients de jeux. Même les serveurs de jeux d'action pour les tireurs et similaires peuvent fonctionner assez bien lorsqu'ils sont écrits en C #, par exemple, car ils poussent rarement le matériel à ses limites comme les clients FPS ont tendance à le faire.

Sean Middleditch
la source
1
+1. De plus, il est possible d'ajouter un attribut comme warn_unused_resultpour fonctionner dans certains compilateurs, ce qui permet d'attraper le code d'erreur non géré.
Maciej Piechotka
Entré ici en voyant C ++ dans le titre, et était totalement sûr que la gestion des erreurs monadiques ne serait pas déjà là. Bon produit!
Adam
Qu'en est-il des endroits où les performances ne sont pas critiques et vous visez principalement une mise en œuvre rapide? par exemple lorsque vous implémentez une logique de jeu?
Ali1S232
qu'en est-il également de l'idée d'utiliser la gestion des exceptions uniquement à des fins de débogage? comme lorsque vous sortez le jeu, vous vous attendez à ce que tout se passe bien sans problème. vous utilisez donc des exceptions pour rechercher et corriger les bogues pendant le développement et, plus tard, en mode édition, supprimez toutes ces exceptions.
Ali1S232
1
@Gajoo: pour la logique du jeu, les exceptions rendent la logique plus difficile à suivre (car elle rend tout le code plus difficile à suivre). Même en Pythnn et C #, les exceptions sont rares pour la logique du jeu, d'après mon expérience. pour le débogage, l'assertion matérielle est généralement beaucoup plus pratique. Arrêtez et arrêtez au moment exact où quelque chose ne va pas, pas après avoir déroulé de grandes portions de la pile et perdu toutes sortes d'informations contextuelles en raison de la sémantique de lancement d'exceptions. Pour la logique de débogage, vous souhaitez créer des outils et des informations / modifier les interfaces graphiques, afin que les concepteurs puissent inspecter et modifier.
Sean Middleditch
13

La vieille sagesse de Donald Knuth lui-même est:

"Nous devons oublier les petites inefficacités, disons environ 97% du temps: l'optimisation prématurée est la racine de tout mal."

Imprimez ceci comme une grande affiche et accrochez-le partout où vous faites une programmation sérieuse.

Donc, même si try / catch est un peu plus lent, je l'utiliserais par défaut:

  • Le code doit être aussi lisible et compréhensible que possible. Vous pourriez écrire le code une fois mais vous le lirez encore plusieurs fois pour déboguer ce code, pour déboguer un autre code, pour comprendre, pour améliorer, ...

  • Exactitude par rapport aux performances. Faire correctement les choses if / else n'est pas anodin. Dans votre exemple, cela n'est pas fait correctement, car pas une seule erreur ne peut être affichée mais plusieurs. Vous devez utiliser en cascade si / alors / sinon.

  • Plus facile à utiliser de manière cohérente: Try / catch adopte un style où vous n'avez pas besoin de vérifier les erreurs à chaque ligne. D'un autre côté: un manquant si / else et votre code pourraient faire des ravages. Bien sûr, uniquement dans des circonstances non reproductibles.

Donc le dernier point:

J'ai entendu que les exceptions sont un peu plus lentes que si

J'ai entendu dire qu'il y a environ 15 ans, je n'ai pas entendu cela de sources crédibles récemment. Les compilateurs peuvent s'être améliorés ou autre chose.

Le point principal est le suivant: ne faites pas d'optimisation prématurée. Ne le faites que lorsque vous pouvez prouver par référence que le code à portée de main est la boucle interne étroite d'un code très utilisé et que le style de commutation améliore considérablement les performances . C'est le cas des 3%.

AH
la source
22
Je vais -1 à l'infini. Je ne peux pas comprendre le mal que cette citation de Knuth a fait au cerveau des développeurs. Considérer si l'utilisation d'exceptions n'est pas une optimisation prématurée, c'est une décision de conception, une décision de conception importante qui a des ramifications bien au-delà des performances.
sam hocevar
3
@SamHocevar: Je suis désolé, je ne comprends pas: la question porte sur les performances possibles lors de l'utilisation d'exceptions. Mon point: n'y pensez pas, le hit n'est pas si mal (le cas échéant) et d'autres choses sont beaucoup plus importantes. Ici, vous semblez être d'accord en ajoutant "décision de conception" à ma liste. D'ACCORD. D'un autre côté, vous dites que la citation de Knuth est mauvaise, ce qui implique qu'une optimisation prématurée n'est pas mauvaise. Mais c'est exactement ce qui s'est passé ici IMO: Le Q ne pense pas à l'architecture, au design ou aux différents algorithmes, seulement aux exceptions et à leur impact sur les performances.
AH
2
Je ne serais pas d'accord avec la clarté en C ++. Le C ++ a des exceptions non vérifiées tandis que certains compilateurs implémentent des valeurs de retour vérifiées. Par conséquent, vous avez du code qui semble plus clair, mais peut avoir une fuite de mémoire cachée ou même mettre le code dans un état non défini. Le code tiers peut également ne pas être protégé contre les exceptions. (La situation est différente en Java / C # qui ont coché les exceptions, GC, ...). À partir de la décision de conception - s'ils ne franchissent pas les points API, la refactorisation de / vers chaque style peut être effectuée de manière semi-automatique avec des perl one-liners.
Maciej Piechotka
1
@SamHocevar: " La question est de savoir ce qui est préférable, pas ce qui est plus rapide. " Relisez le dernier paragraphe de la question. La seule raison pour laquelle il pense même à ne pas utiliser d'exceptions est qu'il pense qu'elles pourraient être plus lentes. Maintenant, si vous pensez qu'il y a d'autres préoccupations que le PO n'a pas prises en compte, n'hésitez pas à les poster ou à voter pour ceux qui l'ont fait. Mais le PO est très clairement axé sur la performance des exceptions.
Nicol Bolas
3
@AH - si vous citez Knuth, n'oubliez pas le reste (et l'OMI la partie la plus importante): " Pourtant, nous ne devons pas laisser passer nos opportunités dans ces 3% critiques. Un bon programmeur ne sera pas bercé de complaisance par un tel raisonnement, il sera sage de regarder attentivement le code critique, mais seulement après que ce code aura été identifié . " Trop souvent, on y voit une excuse pour ne pas optimiser du tout, ou pour essayer de justifier que le code soit lent dans les cas où les performances ne sont pas une optimisation mais sont en fait une exigence fondamentale.
Maximus Minimus
10

Il y a déjà eu une très bonne réponse à cette question, mais je voudrais ajouter quelques réflexions sur la lisibilité du code et la récupération des erreurs dans votre cas particulier.

Je pense que votre code pourrait ressembler à ceci:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

Aucune exception, aucun if. Le rapport d'erreur peut être fait en createText. Et createTextpeut renvoyer un ID de texture par défaut qui ne vous oblige pas à vérifier la valeur de retour, afin que le reste du code fonctionne aussi bien.

sam hocevar
la source