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?
Réponses:
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.
la source
warn_unused_result
pour fonctionner dans certains compilateurs, ce qui permet d'attraper le code d'erreur non géré.La vieille sagesse de Donald Knuth lui-même est:
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 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%.
la source
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:
Aucune exception, aucun if. Le rapport d'erreur peut être fait en
createText
. EtcreateText
peut 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.la source