web-dev-qa-db-fra.com

Quand devrais-je vraiment utiliser noexcept?

Le mot clé noexcept peut être appliqué de manière appropriée à de nombreuses signatures de fonctions, mais je ne suis pas sûr de savoir quand je devrais envisager de l'utiliser dans la pratique. D'après ce que j'ai lu jusqu'à présent, l'ajout de dernière minute de noexcept semble résoudre certains problèmes importants qui se posent lorsque les constructeurs de mouvements lancent. Cependant, je suis toujours incapable de fournir des réponses satisfaisantes à certaines questions pratiques qui m'ont amené à en savoir plus sur noexcept.

  1. Je sais que de nombreux exemples de fonctions ne jetteront jamais, mais pour lesquels le compilateur ne peut pas le déterminer par lui-même. Dois-je ajouter noexcept à la déclaration de fonction dans tous ces cas?

    Devoir réfléchir à la nécessité d'ajouter ou non noexcept après chaque la déclaration de la fonction réduirait considérablement la productivité du programmeur (et, franchement, serait pénible). Pour quelles situations devrais-je faire plus attention à l'utilisation de noexcept, et pour quelles situations puis-je m'en tirer avec la fonction implicite noexcept(false)?

  2. Quand puis-je m'attendre de manière réaliste à une amélioration des performances 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 en raison de la liberté accrue offerte au compilateur d’appliquer en toute sécurité certains types d’optimisations. Les compilateurs modernes tirent-ils parti de noexcept de cette manière? Si non, puis-je m'attendre à ce que certains d'entre eux le fassent dans un proche avenir?

452
void-pointer

Je pense qu'il est trop tôt pour donner une réponse aux "meilleures pratiques", car il n'y a pas eu suffisamment de temps pour l'utiliser dans la pratique. Si cette question était posée à propos des spécificateurs de jet juste après leur publication, les réponses seraient très différentes de ce qu'elles sont maintenant.

Devoir réfléchir à la nécessité d'ajouter ou non noexcept après chaque déclaration de fonction réduirait considérablement la productivité du programmeur (et, franchement, serait pénible).

Alors utilisez-le quand il est évident que la fonction ne lancera jamais.

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

Il semble que les gains d’optimisation les plus importants proviennent des optimisations utilisateur, et non des compilateurs, du fait de la possibilité de vérifier noexcept et de la surcharger. La plupart des compilateurs suivent une méthode de traitement des exceptions sans pénalité si vous ne lancez pas. Je doute donc que cela changerait beaucoup (ou quoi que ce soit) au niveau du code machine de votre code, bien que réduisez peut-être la taille binaire en supprimant le traitement. code.

L'utilisation de noexcept dans le grand 4 (constructeurs, affectation, et non destructeurs comme ils sont déjà noexcept) entraînera probablement les meilleures améliorations car noexcept vérifie que les vérifications sont "communes" dans le code de modèle tel que dans des conteneurs std. Par exemple, std::vector n'utilisera pas le déplacement de votre classe à moins qu'il ne soit marqué noexcept (sinon le compilateur peut le déduire).

167
Pubby

Comme je le répète sans cesse ces jours-ci: la sémantique en premier.

Ajouter noexcept, noexcept(true) et noexcept(false) concerne avant tout la sémantique. Cela ne fait qu'incidemment conditionner un certain nombre d'optimisations possibles.

En tant que programmeur lisant du code, la présence de noexcept ressemble à celle de const: cela m'aide à mieux comprendre ce qui peut ou ne peut pas arriver. Par conséquent, il vaut la peine de prendre le temps de réfléchir pour savoir si vous savez ou non si la fonction va lancer. Pour rappel, tout type d’allocation de mémoire dynamique peut provoquer.


Bon, passons maintenant aux optimisations possibles.

Les optimisations les plus évidentes sont réellement effectuées dans les bibliothèques. C++ 11 fournit un certain nombre de traits qui permettent de savoir si une fonction est noexcept ou non, et l'implémentation de la bibliothèque standard utilisera ces traits pour favoriser les opérations noexcept sur les objets définis par l'utilisateur qu'ils manipulent. , si possible. Tels que déplacer la sémantique .

Le compilateur ne peut peut-être que supprimer un peu de graisse des données de traitement des exceptions, car il doit pour prendre en compte le fait que vous avez peut-être menti. . Si une fonction marquée noexcept lance, alors std::terminate est appelé.

Cette sémantique a été choisie pour deux raisons:

  • bénéficier immédiatement de noexcept même lorsque les dépendances ne l'utilisent pas déjà (compatibilité avec les versions antérieures)
  • permettant la spécification de noexcept lors de l'appel de fonctions qui peuvent théoriquement être lancées mais ne sont pas attendues pour les arguments donnés
123
Matthieu M.

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

Presque toutes les optimisations du compilateur utilisent ce qu'on appelle un "graphe de flux" d'une fonction pour déterminer ce qui est légal. Un graphe de flux est constitué de ce que l’on appelle généralement des "blocs" de la fonction (zones de code qui ont une seule entrée et une seule sortie) et des bords entre les blocs pour indiquer où le flux peut sauter. Noexcept modifie le graphe de flux.

Vous avez demandé un exemple spécifique. Considérons 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 flux de cette fonction est différent si bar est libellé noexcept (l'exécution ne permet pas de sauter entre la fin de 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() à la déclaration de capture. 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 également contenir des constantes. Ce qui était auparavant une évaluation à l'exécution peut être transformé en une évaluation à la compilation, etc.

Quoi qu'il en soit, la réponse courte: noexcept permet au compilateur de générer un graphe de flux plus étroit, qui est utilisé pour raisonner sur toutes sortes d'optimisations courantes du compilateur. Pour un compilateur, les annotations utilisateur de cette nature sont géniales. Le compilateur essaiera de résoudre ce problème, mais ce ne sera généralement pas le cas (la fonction en question peut se trouver dans un autre fichier objet invisible pour le compilateur ou utiliser de manière transitoire une fonction non visible), ou le cas échéant, Une exception triviale qui pourrait être émise et dont vous n'êtes même pas au courant, de sorte qu'elle ne puisse pas la nommer implicitement comme noexcept (allouer de la mémoire peut lancer bad_alloc, par exemple).

73
Terry Mahaffey

noexcept peut considérablement améliorer les performances de certaines opérations. Cela ne se produit pas au niveau de la génération de 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 forte de sécurité contre les exceptions. S'il sait que le constructeur de mouvements de T ne lance pas, il peut simplement déplacer tous les éléments. Sinon, il faut copier tous les Ts. Ceci a été décrit en détail dans this post .

50
Andrzej

Quand puis-je réellement, sauf observer une amélioration des performances 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 est pour compilateur optimisations de performances de la même manière que const est pour optimisations de performances du compilateur. C'est presque jamais.

noexcept est principalement utilisé pour permettre à "vous" de détecter au moment de la compilation si une fonction peut générer une exception. Rappelez-vous: la plupart des compilateurs n'émettent pas de code spécial pour les exceptions à moins qu'il ne lève réellement quelque chose. Donc, noexcept ne consiste pas à donner au compilateur des conseils sur la façon d'optimiser une fonction, mais plutôt à donner vous des conseils sur l'utilisation d'une fonction.

Des modèles tels que move_if_noexcept détectera si le constructeur du déplacement est défini avec noexcept et renverra un const& au lieu d'un && du type s'il ne l'est pas. C'est une façon de dire de bouger s'il est très sécuritaire de le faire.

En général, vous devriez utiliser noexcept lorsque vous pensez que ce sera tile pour le faire. 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 constructeurs appropriés.

En bref: utilisez-le pour les constructeurs de déplacement et les constructions similaires, mais ne vous sentez pas obligé de le rendre dingue.

30
Nicol Bolas
  1. Je sais que de nombreux exemples de fonctions ne jetteront jamais, mais pour lesquels le compilateur ne peut pas le déterminer par lui-même. Devrais-je ajouter 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, vous risqueriez de casser le code existant. Cela pourrait être moins préoccupant lorsque vous implémentez du code uniquement utilisé par votre application.

Si vous avez une fonction qui ne peut pas lancer, demandez-vous si elle aimera rester noexcept ou si cela restreindrait les implémentations futures. Par exemple, vous pouvez souhaiter introduire une vérification d'erreur d'arguments illégaux en lançant des exceptions (par exemple, pour des tests unitaires) ou vous pouvez dépendre d'un autre code de bibliothèque pouvant changer sa spécification d'exception. Dans ce cas, il est plus prudent d'être conservateur et d'omettre noexcept.

D'un autre côté, si vous êtes sûr que la fonction ne doit jamais être lancée et s'il est exact qu'elle fait partie de la spécification, vous devez la déclarer noexcept. Cependant, gardez à l'esprit que le compilateur ne pourra pas 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 tirer avec le noexcept implicite (false)?

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

  1. déplacer des opérations (opérateur d'assignation de déplacement et constructeur de déplacement)
  2. opérations d'échange
  3. désallocateurs de mémoire (opérateur delete, opérateur delete [])
  4. destructeurs (bien que ceux-ci soient implicitement noexcept(true) à moins que vous ne les fabriquiez noexcept(false))

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

Ce type d’optimisation se situe 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 de manière réaliste à une amélioration des performances 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 par rapport à aucune spécification d'exception ou throw() est que le standard donne plus de liberté aux compilateurs pour le déroulement de la pile. Même dans le cas throw(), le compilateur doit complètement dérouler la pile (et il doit le faire dans l'ordre inverse exact des constructions d'objet).

Dans le cas noexcept, par contre, il n'est pas nécessaire de le faire. Il n'est pas nécessaire que la pile soit déroulée (mais le compilateur est toujours autorisé à le faire). Cette liberté permet d’optimiser davantage le code dans la mesure où elle réduit la surcharge liée au fait de pouvoir toujours dérouler la pile.

La question connexe à propos de noexcept, décompression de la pile et performances explique plus en détail la surcharge lorsque la décompression de la pile est requise.

Je recommande également le livre de Scott Meyers "Effective Modern C++", "Point 14: Déclarer qu'il ne fonctionne que s'il n'exécute pas d'exception" pour une lecture plus approfondie.

19
Philipp Claßen

En mots Bjarne:

Lorsque la terminaison est une réponse acceptable, une exception non interceptée y parviendra car elle se transformera en un appel de terminate () (§13.5.2.5). De plus, un spécificateur noexcept (§13.5.1.1) peut rendre ce désir explicite.

Les systèmes à tolérance de pannes qui réussissent sont à plusieurs niveaux. Chaque niveau fait face à autant d'erreurs que possible sans être trop contourné et laisse le reste à des niveaux plus élevés. Les exceptions supportent ce point de vue. De plus, terminate() prend en charge cette vue en fournissant un échappement si le mécanisme de traitement des exceptions est lui-même corrompu ou s'il a été utilisé de manière incomplète. laissant les exceptions non capturées. De même, noexcept fournit un moyen simple d'échapper aux erreurs lorsque la récupération semble impossible.

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

Le constructeur de vecteur peut ne pas réussir à acquérir de la mémoire pour ses dix doublons et lancer un std::bad_alloc. Dans ce cas, le programme se termine. Il se termine sans condition en appelant std::terminate() (§30.4.1.3). Il n'appelle pas les destructeurs d'appeler des fonctions. Il est défini par l'implémentation si les destructeurs des étendues situées entre throw et noexcept (par exemple, pour s dans compute ()) sont appelés. Le programme est sur le point de se terminer, nous ne devrions donc pas dépendre de quelque objet que ce soit. En ajoutant un spécificateur noexcept, nous indiquons que notre code n'a pas été écrit pour faire face à une projection.

17
Saurav Sahu

Je sais que de nombreux exemples de fonctions ne jetteront jamais, mais pour lesquels le compilateur ne peut pas le déterminer par lui-même. Devrais-je ajouter noexcept à la déclaration de fonction dans tous les cas?

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

Il est préférable de déterminer si une fonction peut déclencher 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 lève jamais d'exception" est une contrainte pour l'implémentation. L'omettre ne signifie pas que la fonction peut générer des exceptions. cela signifie que la version actuelle de la fonction et toutes les versions futures peuvent renvoyer des exceptions. C'est une contrainte qui rend la mise en œuvre plus difficile. Mais certaines méthodes doivent avoir la contrainte pour être utiles dans la pratique; plus important encore, ils peuvent donc être appelés à partir de destructeurs, mais également pour l'implémentation de code de "reprise" dans des méthodes fournissant la garantie d'exception forte.

15
Raedwald