Quand devrais-je vraiment utiliser noexcept?

Le mot-clé noexcept peut être appliqué de manière appropriée à de nombreuses signatures de fonction, mais je ne suis pas sûr de savoir quand envisager de l’utiliser en pratique. Sur la base de ce que j’ai lu jusqu’à présent, l’ajout de noexcept à la dernière minute semble résoudre certains problèmes importants qui surviennent lorsque des constructeurs de déménagements lancent. Cependant, je ne suis toujours pas en mesure de fournir des réponses satisfaisantes à certaines questions pratiques qui m’ont amené à en lire plus sur noexcept en premier lieu.

  1. Il y a beaucoup d’exemples de fonctions que je sais ne jamais lancer, mais pour lesquelles le compilateur ne peut pas le déterminer seul. Dois-je append noexcept à la déclaration de fonction dans tous les cas?

    noexcept à la nécessité d’append ou de ne pas append noexcept après chaque déclaration de fonction réduirait considérablement la productivité du programmeur (et franchement, ce serait noexcept ). Pour quelles situations devrais-je faire plus attention à l’utilisation de noexcept , et pour quelles situations puis-je m’en sortir avec l’implicite noexcept(false) ?

  2. Quand puis-je m’attendre à une amélioration de la performance après l’utilisation de noexcept ? En particulier, donnez un exemple de code pour lequel un compilateur C ++ est capable de générer un meilleur code machine après l’ajout de noexcept .

    Personnellement, je me soucie de noexcept raison de la liberté accrue offerte au compilateur pour appliquer certains types d’optimisations en toute sécurité. Est-ce que les compilateurs modernes profitent de cette manière? Si non, puis-je m’attendre à ce que certains d’entre eux le fassent dans un avenir proche?

Je pense qu’il est trop tôt pour donner une réponse aux «meilleures pratiques» car il n’y a pas eu assez de temps pour l’utiliser dans la pratique. Si cela était demandé à propos des spécificateurs de jet juste après leur sortie, les réponses seraient très différentes de celles de maintenant.

noexcept à la nécessité d’append ou non noexcept après chaque déclaration de fonction réduirait considérablement la productivité du programmeur (et franchement, ce serait noexcept ).

Eh bien, alors utilisez-le quand il est évident que la fonction ne sera jamais lancée.

Quand puis-je m’attendre à une amélioration de la performance après l’utilisation de noexcept ? […] Personnellement, je me soucie de noexcept raison de la plus grande liberté offerte au compilateur pour appliquer certains types d’optimisations en toute sécurité.

Il semble que les gains d’optimisation les plus importants proviennent des optimisations des utilisateurs, et non des compilateurs, en raison de la possibilité de vérifier l’ noexcept et de la surcharger. La plupart des compilateurs suivent une méthode de gestion des exceptions sans pénalité si vous ne lancez pas, donc je doute que cela changerait beaucoup (ou quelque chose) au niveau du code machine de votre code, bien que peut-être réduire la taille binary en supprimant le traitement code.

Utiliser noexcept dans le big 4 (constructeurs, affectations, pas les destructeurs, car ils sont déjà noexcept ) entraînera probablement les meilleures améliorations car les contrôles noexcept sont «communs» dans le code du modèle, comme dans les conteneurs std. Par exemple, std::vector n’utilisera le mouvement de votre classe que s’il est marqué noexcept (ou si le compilateur peut le déduire autrement).

Comme je répète ces jours-ci: la sémantique d’abord .

Ajouter noexcept , noexcept(true) et noexcept(false) concerne avant tout la sémantique. Cela ne conditionne qu’accessoirement plusieurs optimisations possibles.

En tant que programmeur lisant du code, la présence de noexcept s’apparente à celle de const : cela m’aide à mieux comprendre ce qui peut arriver ou non. Par conséquent, il est utile de se demander si vous savez si la fonction sera lancée ou non. Pour rappel, toute allocation de mémoire dynamic peut être lancée.


Ok, passons maintenant aux optimisations possibles.

Les optimisations les plus évidentes sont effectivement effectuées dans les bibliothèques. C ++ 11 fournit un certain nombre de caractéristiques permettant de savoir si une fonction est noexcept ou non, et l’implémentation de la bibliothèque standard elle-même utilisera ces caractéristiques pour favoriser les opérations sans noexcept sur les objects définis par l’utilisateur qu’ils manipulent, si possible. Comme la sémantique de mouvement .

Le compilateur peut ne raser qu’un peu (peut-être) des données de gestion des exceptions, car il doit tenir compte du fait que vous avez peut-être menti. Si une fonction marquée noexcept est lancée, alors std::terminate est appelé.

Ces sémantiques ont été choisies pour deux raisons:

  • bénéficiant immédiatement de noexcept même lorsque les dépendances ne l’utilisent pas déjà (compatibilité descendante)
  • permettre la spécification de noexcept lors de l’appel de fonctions qui peuvent théoriquement être noexcept mais dont on ne s’attend pas à ce qu’elles soient pour les arguments donnés

Cela fait une différence (potentiellement) énorme pour l’optimiseur dans le compilateur. Les compilateurs ont effectivement cette fonctionnalité pendant des années via l’instruction throw () vide après une définition de fonction, ainsi que des extensions de propriété. Je peux vous assurer que les compilateurs modernes tirent parti de ces connaissances pour générer un meilleur code.

Presque chaque optimisation dans le compilateur utilise quelque chose appelé “graphe de stream” d’une fonction pour raisonner sur ce qui est légal. Un graphe de stream est constitué de ce que l’on appelle généralement des “blocs” de la fonction (zones de code ayant une seule entrée et une seule sortie) et des arêtes entre les blocs pour indiquer où le stream peut passer. Noexcept modifie le graphe de stream.

Vous avez demandé un exemple spécifique. Considérez ce code:

 void foo(int x) { try { bar(); x = 5; // other stuff which doesn't modify x, but might throw } catch(...) { // don't modify x } baz(x); // or other statement using x } 

Le graphe de stream pour cette fonction est différent si la bar est étiquetée noexcept (il n’y a aucun moyen d’exécuter entre la fin de la bar et l’instruction catch). Lorsqu’il est étiqueté comme noexcept , le compilateur est certain que la valeur de x est 5 pendant la fonction baz – le bloc x = 5 est dit “dominer” le bloc baz (x) sans le bord de bar() à l’instruction catch. Il peut alors faire quelque chose appelé “propagation constante” pour générer un code plus efficace. Ici, si baz est en ligne, les instructions utilisant x peuvent aussi contenir des constantes et ensuite ce qui était une évaluation à l’exécution peut être transformé en une évaluation à la compilation, etc.

Quoi qu’il en soit, réponse courte: noexcept permet au compilateur de générer un graphique de stream plus serré, et le graphe de stream sert à raisonner sur toutes sortes d’optimisations de compilateur courantes. Pour un compilateur, les annotations de cette nature sont géniales. Le compilateur essaiera de comprendre ce genre de choses, mais il ne peut généralement pas (la fonction en question pourrait se trouver dans un autre fichier object non visible par le compilateur ou utiliser de manière transitoire certaines fonctions non visibles) ou exception sortingviale qui pourrait être lancée et que vous ne connaissez même pas, elle ne peut donc pas l’ noexcept implicitement comme noexcept (allouer de la mémoire peut lancer bad_alloc, par exemple).

noexcept peut considérablement améliorer les performances de certaines opérations. Cela ne se produit pas au niveau de la génération du code machine par le compilateur, mais en sélectionnant l’algorithme le plus efficace: comme d’autres l’ont mentionné, vous faites cette sélection en utilisant la fonction std::move_if_noexcept . Par exemple, la croissance de std::vector (par exemple, lorsque nous appelons reserve ) doit fournir une garantie de sécurité exceptionnelle. S’il sait que le constructeur de mouvement de T ne lance pas, il peut simplement déplacer chaque élément. Sinon, il doit copier tous les T Cela a été décrit en détail dans cet article .

Quand puis-je être réaliste, sauf pour observer une amélioration de la performance après avoir utilisé noexcept ? En particulier, donnez un exemple de code pour lequel un compilateur C ++ est capable de générer un meilleur code machine après l’ajout de noexcept.

Euh, jamais? N’est jamais un temps? Jamais.

noexcept d’optimiser les performances du compilateur de la même manière que const pour l’optimisation des performances du compilateur. C’est presque jamais.

noexcept est principalement utilisé pour permettre à “vous” de détecter à la compilation si une fonction peut lancer une exception. Rappelez-vous: la plupart des compilateurs n’émettent pas de code spécial pour les exceptions à moins que cela ne jette réellement quelque chose. Donc, noexcept ne consiste pas à donner au compilateur des conseils sur la façon d’optimiser une fonction, mais vous donne également des conseils sur l’utilisation d’une fonction.

Des modèles tels que move_if_noexcept détecteront si le constructeur de déplacement est défini avec noexcept et noexcept un const& non un const& si ce n’est pas le cas. C’est une façon de dire de bouger s’il est très prudent de le faire.

En général, vous devez utiliser noexcept lorsque vous pensez que cela sera réellement utile . Certains codes prendront des chemins différents si is_nothrow_constructible est vrai pour ce type. Si vous utilisez du code qui le fera, alors n’hésitez pas à noexcept pas noexcept les constructeurs appropriés.

En bref: utilisez-le pour les constructeurs de déménagements et les constructions similaires, mais ne vous sentez pas obligé de devenir fou.

  1. Il y a beaucoup d’exemples de fonctions que je sais ne jamais lancer, mais pour lesquelles le compilateur ne peut pas le déterminer seul. Dois-je append noexcept à la déclaration de fonction dans tous les cas?

noexcept est délicat, car il fait partie de l’interface des fonctions. En particulier, si vous écrivez une bibliothèque, votre code client peut dépendre de la propriété noexcept . Il peut être difficile de le changer plus tard, car vous risquez de casser le code existant. Cela peut être moins inquiétant lorsque vous implémentez du code uniquement utilisé par votre application.

Si vous avez une fonction qui ne peut pas être lancée, demandez-vous si vous souhaitez restr à l’ noexcept ou cela limitera-t-il les futures implémentations? Par exemple, vous pourriez vouloir introduire la vérification des erreurs des arguments illégaux en lançant des exceptions (par exemple, pour les tests unitaires) ou vous pourriez dépendre d’un autre code de bibliothèque qui pourrait modifier sa spécification d’exception. Dans ce cas, il est plus prudent d’être prudent et de ne pas noexcept .

Par contre, si vous êtes certain que la fonction ne doit jamais être lancée et que cela fait partie de la spécification, vous devez le déclarer sans noexcept . Cependant, gardez à l’esprit que le compilateur ne sera pas en mesure de détecter les violations de noexcept si votre implémentation change.

  1. Pour quelles situations devrais-je faire plus attention à l’utilisation de noexcept, et pour quelles situations puis-je m’en sortir avec l’implicite noexcept (false)?

Il y a quatre classes de fonctions sur lesquelles vous devriez vous concentrer, car elles auront probablement le plus d’impact:

  1. déplacer des opérations (déplacer l’opérateur d’affectation et déplacer des constructeurs)
  2. opérations de swap
  3. déstockeurs de mémoire (opérateur delete, opérateur delete [])
  4. les destructeurs (bien que ceux-ci soient implicitement noexcept(true) moins que vous ne leur noexcept(false) )

Ces fonctions doivent généralement être sans noexcept , et il est fort probable que les implémentations de bibliothèque puissent utiliser la propriété noexcept . Par exemple, std::vector peut utiliser des opérations de déplacement non-lancantes sans sacrifier de garanties d’exception fortes. Sinon, il faudra utiliser des éléments de copie (comme en C ++ 98).

Ce type d’optimisation est au niveau algorithmique et ne repose pas sur des optimisations du compilateur. Cela peut avoir un impact significatif, surtout si les éléments sont coûteux à copier.

  1. Quand puis-je m’attendre à une amélioration de la performance après l’utilisation de noexcept? En particulier, donnez un exemple de code pour lequel un compilateur C ++ est capable de générer un meilleur code machine après l’ajout de noexcept.

L’avantage de noexcept sans spécification d’exception ou throw() est que la norme permet aux compilateurs plus de liberté pour le déroulement de la stack. Même dans le cas de throw() , le compilateur doit dérouler complètement la stack (et il doit le faire dans l’ordre inverse exact des constructions d’object).

Dans le cas noexcept , par contre, il n’est pas nécessaire de le faire. Il n’est pas nécessaire que la stack soit déroulée (mais le compilateur est toujours autorisé à le faire). Cette liberté permet une optimisation du code supplémentaire, car elle réduit la surcharge de pouvoir toujours dérouler la stack.

La question connexe à propos de noexcept, du déroulement de la stack et des performances passe par plus de détails sur la surcharge lorsque le déroulement de la stack est requirejs.

Je recommande également le livre de Scott Meyers “Effective Modern C ++”, “Point 14: Déclarer les fonctions noexcept si elles n’émettent pas d’exceptions” pour une lecture ultérieure.

En mots Bjarne:

Lorsque la résiliation est une réponse acceptable, une exception non capturée y parviendra car elle se transforme en appel de terminate () (§13.5.2.5). De même, un spécificateur non-conforme (§13.5.1.1) peut rendre ce désir explicite.

Les systèmes à tolérance de pannes réussis sont multi-niveaux. Chaque niveau fait face à autant d’erreurs qu’il peut sans se déformer et laisse le rest à des niveaux plus élevés. Les exceptions soutiennent cette vue. De plus, terminate() prend en charge cette vue en fournissant une échappée si le mécanisme de gestion des exceptions lui-même est corrompu ou s’il a été incomplètement utilisé, laissant ainsi des exceptions non détectées. De même, noexcept fournit une solution simple pour les erreurs lorsque la récupération semble impossible.

  double compute(double x) noexcept; { ssortingng s = "Courtney and Anya"; vector tmp(10); // ... } 

Le constructeur vectoriel peut ne pas acquérir la mémoire pour ses dix doubles et lancer un std::bad_alloc . Dans ce cas, le programme prend fin. Il se termine sans condition en invoquant std::terminate() (§30.4.1.3). Il n’invoque pas les destructeurs des fonctions d’appel. Il est défini par l’implémentation si les destructeurs des étendues entre le throw et le noexcept (par exemple, pour s dans compute ()) sont invoqués. Le programme est sur le sharepoint se terminer, nous ne devrions donc pas dépendre d’un object. En ajoutant un spécificateur noexcept , nous indiquons que notre code n’a pas été écrit pour faire face à un jet.

Il y a beaucoup d’exemples de fonctions que je sais ne jamais lancer, mais pour lesquelles le compilateur ne peut pas le déterminer seul. Dois-je append noexcept à la déclaration de fonction dans tous les cas?

Lorsque vous dites “je sais qu’ils ne lanceront jamais”, vous voulez dire en examinant la mise en œuvre de la fonction que vous savez que la fonction ne lancera pas. Je pense que cette approche est à l’envers.

Il est préférable de se demander si une fonction peut émettre des exceptions pour faire partie de la conception de la fonction: aussi importante que la liste des arguments et si une méthode est un mutateur (… const ). Déclarer que “cette fonction ne lance jamais d’exceptions” est une contrainte pour l’implémentation. L’omettre ne signifie pas que la fonction peut émettre des exceptions; cela signifie que la version actuelle de la fonction et toutes les versions futures peuvent contenir des exceptions. C’est une contrainte qui rend la mise en œuvre plus difficile. Mais certaines méthodes doivent avoir la contrainte d’être pratiquement utiles; plus important encore, ils peuvent être appelés depuis des destructeurs, mais aussi pour l’implémentation du code “roll-back” dans les méthodes qui fournissent la garantie d’exception forte.

Etant donné que le nombre de règles à travailler avec c ++ ne cesse d’augmenter et que nous avons vécu heureux sans exception pendant des années, je pense que si quelqu’un ne donne pas de raisons explicites pour l’utiliser, personne ne le fera. développeurs