web-dev-qa-db-fra.com

Si vous êtes dans le camp «nous n'utilisons pas d'exceptions», comment utilisez-vous la bibliothèque standard?

Note: Je ne joue pas l'avocat du diable ou quelque chose comme ça ici - je suis juste vraiment curieux puisque je ne suis pas moi-même dans ce camp.

La plupart des types de la bibliothèque standard ont soit des fonctions de mutation qui peuvent lever des exceptions (par exemple si l'allocation de mémoire échoue) soit des fonctions non-mutantes qui peuvent lever des exceptions (par exemple des accesseurs indexés hors limites). En plus de cela, de nombreuses fonctions gratuites peuvent lever des exceptions (par exemple operator new et dynamic_cast<T&>).

Comment gérez-vous pratiquement dans ce contexte "nous n'utilisons pas d'exceptions"?

  • Essayez-vous jamais d'appeler une fonction qui peut lancer? (Je ne vois pas comment cela évoluerait, donc je suis très intéressé de savoir comment vous y parvenez si tel est le cas)

  • Êtes-vous d'accord avec le lancement de bibliothèque standard et vous traitez "nous n'utilisons pas d'exceptions" comme "nous n'avons jamais jeter exceptions de nos code et nous n'avons jamais = catch exceptions à d'autres code "?

  • Désactivez-vous complètement la gestion des exceptions via les commutateurs du compilateur? Dans l'affirmative, comment fonctionnent les parties génératrices d'exceptions de la bibliothèque standard?

  • EDIT Vos constructeurs peuvent-ils échouer, ou utilisez-vous par convention une construction en 2 étapes avec une fonction d'initialisation dédiée qui peut retourner un code d'erreur en cas d'échec ( que le constructeur ne peut pas), ou faites-vous autre chose?

MODIFIER Précision mineure 1 semaine après le début de la question ... Une grande partie du contenu des commentaires et des questions ci-dessous se concentre sur le pourquoi aspects des exceptions vs "autre chose". Je ne m'intéresse pas à cela, mais quand vous choisissez de faire "autre chose", comment traitez-vous avec les parties de bibliothèque standard qui faire lever des exceptions?

59
Johann Gerell

Je répondrai pour moi et mon coin du monde. J'écris c ++ 14 (il aura 17 ans une fois que les compilateurs auront un meilleur support) des applications financières critiques de latence qui traitent des sommes d'argent gigantesques et ne peuvent jamais baisser. L'ensemble de règles est le suivant:

  • aucune exception
  • pas de rtti
  • pas de répartition d'exécution
  • (presque) pas d'héritage

La mémoire est regroupée et pré-allouée, il n'y a donc pas d'appels malloc après l'initialisation. Les structures de données sont soit immortelles soit trivialement copiables, donc les destructeurs sont presque absents (il y a quelques exceptions, comme les gardes de portée). Fondamentalement, nous faisons de la sécurité de type C + des modèles + des lambdas. Bien sûr, les exceptions sont désactivées via le commutateur du compilateur. Quant à la STL, les bonnes parties de celle-ci (à savoir: algorithme, numérique, type_traits, itérateur, atomique, ...) sont toutes utilisables. Les parties générant des exceptions coïncident avec les parties d'allocation de mémoire d'exécution et les parties semi-OO, nous pouvons donc nous débarrasser de toutes les failles en une seule fois: flux, conteneurs sauf std :: array, std :: string.

Pourquoi faire ceci?

  1. Parce que comme OO, l'exception offre une propreté illusoire en cachant ou en déplaçant le problème ailleurs, et rend le reste du programme plus difficile à diagnostiquer. Lorsque vous compilez sans "-fno-exceptions", toutes vos fonctions propres et bien comportées doivent supporter le soupçon d'être disponibles. Il est beaucoup plus facile d'avoir une vérification approfondie de la santé mentale autour du périmètre de votre base de code, que de rendre chaque opération accessible.
  2. Parce que les exceptions sont essentiellement des GOTO à longue portée qui ont une destination non spécifiée. Vous n'utiliserez pas longjmp (), mais les exceptions sont sans doute bien pires.
  3. Parce que les codes d'erreur sont supérieurs. Vous pouvez utiliser [[nodiscard]] pour forcer le code appelant à vérifier.
  4. Parce que les hiérarchies d'exceptions ne sont pas nécessaires. La plupart du temps, il est peu logique de distinguer ce qui a commis une erreur, et quand c'est le cas, c'est probablement parce que différentes erreurs nécessitent un nettoyage différent et il aurait été beaucoup mieux de signaler explicitement.
  5. Parce que nous avons des invariants complexes à maintenir. Cela signifie qu'il existe un code, même au fond des entrailles, qui doit avoir des garanties transnationales. Il y a deux façons de procéder: soit vous rendez vos procédures impératives aussi pures que possible (c'est-à-dire: assurez-vous de ne jamais échouer), soit vous avez des structures de données immuables (c'est-à-dire: rend possible la récupération après défaillance). Si vous avez des structures de données immuables, vous pouvez bien sûr avoir des exceptions, mais vous ne les utiliserez pas parce que vous utiliserez des types de somme. Les structures de données fonctionnelles sont cependant lentes, donc l'autre alternative est d'avoir des fonctions pures et de le faire dans un langage sans exception tel que C, no-except C++ ou Rust. Peu importe la beauté de D, tant qu'il n'est pas nettoyé du GC et des exceptions, c'est une non-option.
  6. Avez-vous déjà testé vos exceptions comme vous le feriez avec un chemin de code explicite? Qu'en est-il des exceptions qui "ne peuvent jamais arriver"? Bien sûr que non, et quand vous frappez réellement ces exceptions, vous êtes foutu.
  7. J'ai vu du "beau" code neutre en exception en C++. Autrement dit, il fonctionne de manière optimale sans cas Edge, que le code qu'il appelle utilise ou non des exceptions. Ils sont vraiment difficiles à écrire et je soupçonne, difficiles à modifier si vous souhaitez conserver toutes vos garanties d'exception. Cependant, je n'ai vu aucun "beau" code qui lève ou intercepte des exceptions. Tout le code que j'ai vu qui interagit directement avec les exceptions a été universellement laid. La quantité d'efforts consacrés à l'écriture de code neutre par rapport aux exceptions éclipse complètement la quantité d'efforts qui a été enregistrée à partir du code merdique qui lève ou intercepte des exceptions. "Beau" est entre guillemets car ce n'est pas de la beauté réelle: il est généralement fossilisé car son édition nécessite le fardeau supplémentaire du maintien de la neutralité d'exception. Si vous ne disposez pas de tests unitaires qui abusent délibérément et de manière exhaustive des exceptions pour déclencher ces cas Edge, même le "beau" code neutre d'exception se désintègre en fumier.
42
KevinZ

Dans notre cas, nous désactivons les exceptions via le compilateur (par exemple -fno-exceptions pour gcc).

Dans le cas de gcc, ils utilisent une macro appelée _GLIBCXX_THROW_OR_ABORT qui est défini comme

#ifndef _GLIBCXX_THROW_OR_ABORT
# if __cpp_exceptions
#  define _GLIBCXX_THROW_OR_ABORT(_EXC) (throw (_EXC))
# else
#  define _GLIBCXX_THROW_OR_ABORT(_EXC) (__builtin_abort())
# endif
#endif

(vous pouvez le trouver dans libstdc++-v3/include/bits/c++config sur les dernières versions de gcc).

Ensuite, il suffit de faire face au fait que les exceptions levées sont simplement abandonnées. Vous pouvez toujours capter le signal et imprimer la pile (il y a une bonne réponse sur SO qui explique cela), mais vous feriez mieux d'éviter ce genre de choses qui se produisent (au moins dans les versions).

Si vous voulez un exemple, au lieu d'avoir quelque chose comme

try {
   Foo foo = mymap.at("foo");
   // ...
} catch (std::exception& e) {}

tu peux faire

auto it = mymap.find("foo");
if (it != mymap.end()) {
    Foo foo = it->second;
    // ...
}
20
Zouch

Je tiens également à souligner que lorsque vous demandez de ne pas utiliser d'exceptions, il y a une question plus générale sur la bibliothèque standard: Êtes-vous en utilisant la bibliothèque standard lorsque vous êtes dans l'un des "nous n'utilisons pas exceptions "camps?

La bibliothèque standard est lourde. Dans certains camps "nous n'utilisons pas d'exceptions", comme de nombreuses sociétés GameDev par exemple, des alternatives mieux adaptées pour STL sont utilisées - principalement basées sur EASTL ou TTL. Ces bibliothèques n'utilisent pas les exceptions de toute façon et c'est parce que les consoles de huitième génération ne les gèrent pas trop bien (ou même pas du tout). Pour un code de production de pointe AAA, les exceptions sont de toute façon trop lourdes, c'est donc un scénario gagnant-gagnant dans de tels cas.

En d'autres termes, pour de nombreux programmeurs, désactiver les exceptions va de pair avec ne pas utiliser du tout STL.

18
abl

Remarque J'utilise des exceptions ... mais j'ai été forcé de ne pas le faire.

Essayez-vous de ne jamais appeler une fonction qui peut lancer? (Je ne vois pas comment cela évoluerait, donc je suis très intéressé de savoir comment vous y parvenez si tel est le cas)

Cela serait probablement irréalisable, du moins à grande échelle. De nombreuses fonctions peuvent atterrir, les éviter paralysent entièrement votre base de code.

Êtes-vous d'accord avec le lancement de bibliothèque standard et vous traitez "nous n'utilisons pas d'exceptions" comme "nous ne lançons jamais d'exceptions de notre code et nous n'attrapons jamais d'exceptions du code d'autrui"?

Vous devez à peu près être d'accord avec ça ... Si le code de bibliothèque va lever une exception et que votre code ne va pas la gérer, la terminaison est le comportement par défaut.

Désactivez-vous complètement la gestion des exceptions via les commutateurs du compilateur? Dans l'affirmative, comment fonctionnent les parties génératrices d'exceptions de la bibliothèque standard?

C'est possible (à l'époque, c'était parfois populaire pour certains types de projets); Les compilateurs prennent en charge/peuvent prendre en charge cette fonctionnalité, mais vous devrez consulter leur documentation pour savoir quels seraient et pourraient être les résultats (et quelles fonctionnalités linguistiques sont prises en charge dans ces conditions).

En général, lorsqu'une exception est levée, le programme doit abandonner ou quitter autrement. Certaines normes de codage l'exigent encore, la norme de codage JSF vient à l'esprit (IIRC).

Stratégie générale pour ceux qui "n'utilisent pas d'exceptions"

La plupart des fonctions ont un ensemble de conditions préalables qui peuvent être vérifiées avant l'appel. Vérifiez-les. S'ils ne sont pas respectés, ne passez pas l'appel; revenir à ce que la gestion des erreurs est dans ce code. Pour ces fonctions que vous ne pouvez pas vérifier pour vous assurer que les conditions préalables sont remplies ... pas grand-chose, le programme sera probablement abandonné.

Vous pourriez chercher à éviter les bibliothèques qui lèvent des exceptions - vous l'avez demandé dans le contexte de la bibliothèque standard, donc cela ne fonctionne pas ' t tout à fait adapté au projet de loi, mais il reste une option.

Autres stratégies possibles; Je sais que cela semble banal, mais choisissez une langue qui ne les utilise pas. C pourrait bien faire ...

... au cœur de ma question (votre interaction avec la bibliothèque standard, le cas échéant), je suis très intéressé à entendre parler de vos constructeurs. Peuvent-ils échouer, ou utilisez-vous par convention une construction en 2 étapes avec une fonction d'initialisation dédiée qui peut retourner un code d'erreur en cas d'échec (ce que le constructeur ne peut pas)? Ou quelle est votre stratégie là-bas?

Si des constructeurs sont utilisés, deux approches sont généralement utilisées pour indiquer l'échec;

  1. Définissez un code d'erreur interne ou enum pour indiquer l'échec et sa nature. Cela peut être interrogé après la construction de l'objet et les mesures appropriées prises.
  2. N'utilisez pas de constructeur (ou du moins ne construisez que ce qui ne peut pas échouer dans le constructeur - le cas échéant), puis utilisez une méthode init() quelconque pour faire (ou terminer) la construction. La méthode membre peut alors renvoyer une erreur en cas d'échec.

L'utilisation de la technique init() est généralement privilégiée car elle peut être chaînée et évolue mieux que le code "erreur" interne.

Encore une fois, ce sont des techniques qui proviennent d'environnements où il n'y a pas d'exceptions (comme C). L'utilisation d'un langage tel que C++ sans exception limite son utilisation et l'utilité de l'étendue de la bibliothèque standard.

10
Niall

N'essayant pas de répondre pleinement aux questions que vous avez posées, je vais simplement donner à Google un exemple de base de code qui n'utilise pas les exceptions comme mécanisme pour traiter les erreurs.

Dans la base de code Google C++, toutes les fonctions qui peuvent échouer retournent un objet status qui a des méthodes comme ok pour spécifier le résultat de l'appelé.
Ils ont configuré GCC pour échouer la compilation si le développeur a ignoré l'objet return status.

De plus, à partir du petit code open source qu'ils fournissent (comme la bibliothèque LevelDB), il semble qu'ils n'utilisent pas beaucoup STL de toute façon, donc la gestion des exceptions devient rare. comme le dit Titus Winters dans ses conférences à CPPCon, ils "respectent la norme, mais ne l'idolâtrent pas".

8
David Haim

Je pense que c'est une question d'attitude. Vous devez être dans le camp de "Je m'en fiche si quelque chose échoue". Cela se traduit généralement par du code, pour lequel on a besoin d'un débogueur (sur le site du client) pour savoir pourquoi tout à coup quelque chose ne fonctionne plus. De même, potentiellement les personnes qui font de l '"ingénierie" logicielle de cette manière, n'utilisent pas de code très complexe. Par exemple. on serait incapable d'écrire du code, qui repose sur le fait qu'il n'est exécuté que si toutes les n ressources sur lesquelles il s'appuie ont été allouées avec succès (tout en utilisant RAII pour ces ressources). Ainsi: Un tel codage entraînerait soit:

  • une quantité de code ingérable pour la gestion des erreurs
  • une quantité de code ingérable pour éviter l'exécution de code, qui repose sur une allocation réussie de certaines ressources
  • pas de gestion des erreurs et donc beaucoup plus de support et de temps de développement

Notez que je parle de code moderne, de chargement de DLL fournies par le client à la demande et d'utilisation de processus enfants. Il existe de nombreuses interfaces sur lesquelles quelque chose peut échouer. Je ne parle pas d'un remplacement pour grep/more/ls/find.

0
user8434768