Le noexcept
mot-clé peut être appliqué de manière appropriée à de nombreuses signatures de fonction, mais je ne sais pas quand je devrais envisager de l'utiliser dans la pratique. Sur la base de ce que j'ai lu jusqu'à présent, l'ajout de dernière minute noexcept
semble résoudre certains problèmes importants qui surviennent lorsque les constructeurs de mouvements lancent. Cependant, je ne suis toujours pas en mesure de fournir des réponses satisfaisantes à certaines questions pratiques qui m'ont amené à en savoir plus noexcept
en premier lieu.
Il existe de nombreux exemples de fonctions que je sais ne lanceront jamais, mais pour lesquelles le compilateur ne peut pas le déterminer par lui-même. Dois-je joindre
noexcept
à la déclaration de fonction dans tous ces cas?Devoir penser à savoir si je dois ou non ajouter
noexcept
après chaque déclaration de fonction réduirait considérablement la productivité du programmeur (et franchement, ce serait pénible). Pour quelles situations devrais-je être plus prudent quant à l'utilisation denoexcept
, et pour quelles situations puis-je m'en tirer avec les implicitesnoexcept(false)
?Quand puis-je m'attendre de manière réaliste à observer une amélioration des performances après utilisation
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 denoexcept
.Personnellement, je me soucie
noexcept
de la liberté accrue accordée au compilateur pour appliquer en toute sécurité certains types d'optimisations. Les compilateurs modernes profitent-ils denoexcept
cette manière? Sinon, puis-je m'attendre à ce que certains le fassent dans un proche avenir?
move_if_nothrow
(ou whatchamacallit) verra une amélioration des performances s'il y a un ctor de mouvement sans exception.move_if_noexcept
.Réponses:
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 lanceurs de spécification juste après leur sortie, les réponses seraient très différentes de celles d'aujourd'hui.
Eh bien, utilisez-le quand il est évident que la fonction ne lancera jamais.
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 les vérifier
noexcept
et de les 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 quoi que ce soit) au niveau du code machine de votre code, bien que peut-être réduire la taille binaire en supprimant le code de manipulation.L'utilisation
noexcept
dans les quatre grands (constructeurs, affectations, et non destructeurs comme ils le sont déjànoexcept
) entraînera probablement les meilleures améliorations car lesnoexcept
vérifications sont «courantes» dans le code du modèle, comme dans lesstd
conteneurs. Par exemple,std::vector
n'utilisera pas le déplacement de votre classe à moins qu'il ne soit marquénoexcept
(ou que le compilateur ne puisse le déduire autrement).la source
std::terminate
astuce obéit toujours au modèle Zero-Cost. Autrement dit, il se trouve que la plage d'instructions dans lesnoexcept
fonctions est mappée pour appelerstd::terminate
if si ellethrow
est utilisée à la place du dérouleur de pile. Je doute donc qu'il ait plus de frais généraux que le suivi d'exceptions régulier.noexcept
garantit.noexcept
fonction est lancée, ellestd::terminate
est appelée, ce qui semble impliquer une petite surcharge" ... Non, cela devrait être implémenté en ne générant pas de tables d'exceptions pour une telle fonction, que le répartiteur d'exceptions devrait intercepter puis renflouer.noexcept
fait partie de l' interface de la fonction ; vous ne devez pas l'ajouter simplement parce que votre implémentation actuelle ne se produit pas. Je ne suis pas sûr de la bonne réponse à cette question, mais je suis tout à fait convaincu que la façon dont votre fonction se comporte aujourd'hui n'a rien à voir avec cela ...Comme je ne cesse de répéter ces jours-ci: la sémantique d'abord .
Ajouter
noexcept
,noexcept(true)
etnoexcept(false)
c'est avant tout de la sémantique. Elle ne conditionne qu'incidemment un certain nombre d'optimisations possibles.En tant que programmeur lisant du code, la présence de
noexcept
s'apparente à celle deconst
: cela m'aide à mieux comprendre ce qui peut ou non arriver. Par conséquent, il vaut la peine de passer du temps à réfléchir à savoir si vous savez si la fonction lancera. Pour rappel, tout type d'allocation dynamique de mémoire peut être lancé.Bon, passons maintenant aux optimisations possibles.
Les optimisations les plus évidentes sont en fait effectuées dans les bibliothèques. C ++ 11 fournit un certain nombre de traits qui permettent de savoir si une fonction l'est
noexcept
ou non, et l'implémentation de la bibliothèque standard utilisera ces traits pour favoriser lesnoexcept
opérations sur les objets définis par l'utilisateur qu'ils manipulent, si possible. Tels que déplacer la sémantique .Le compilateur peut seulement raser un peu de graisse (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
lance, ellestd::terminate
est appelée.Cette sémantique a été choisie pour deux raisons:
noexcept
même lorsque les dépendances ne l'utilisent pas déjà (compatibilité descendante)noexcept
lors de l'appel de fonctions qui peuvent théoriquement lancer, mais ne sont pas censées le faire pour les arguments donnésla source
noexcept
fonctions n'aurait pas besoin de faire quoi que ce soit de spécial, car toute exception qui pourrait survenir se déclencheterminate
avant d'atteindre ce niveau. Cela diffère grandement d'avoir à traiter et propager unebad_alloc
exception.noexcept
est clairement utile / une bonne idée. Je commence à penser que la construction de déménagements, l'affectation de déménagements et l'échange sont les seuls cas qui existent ... Connaissez-vous d'autres cas?noexcept
quelqu'un peut l'utiliser en toute confiance sur une donnée qui pourrait être consultée par la suite. Je pourrais voir cette idée utilisée ailleurs, mais la bibliothèque Standard est plutôt mince en C ++ et elle n'est utilisée que pour optimiser les copies d'éléments, je pense.Cela fait réellement une différence (potentiellement) énorme pour l'optimiseur dans le compilateur. Les compilateurs ont en fait cette fonctionnalité depuis 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é "graphique de flux" d'une fonction pour raisonner sur ce qui est légal. Un graphe de flux se compose 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 aller. Noexcept modifie le graphique de flux.
Vous avez demandé un exemple précis. Considérez ce code:
Le graphique de flux de cette fonction est différent s'il
bar
est étiqueténoexcept
(il n'y a aucun moyen pour l'exécution de sauter entre la fin debar
l'instruction catch). Lorsqu'il est étiqueté commenoexcept
, le compilateur est certain que la valeur de x est 5 pendant la fonction baz - le bloc x = 5 est censé "dominer" le bloc baz (x) sans le bord debar()
à 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 également contenir des constantes et ce qui était auparavant une évaluation d'exécution peut être transformé en une évaluation au moment de la compilation, etc.
Quoi qu'il en soit, la réponse courte:
noexcept
permet au compilateur de générer un graphique de flux plus serré, et le graphique de flux est utilisé pour raisonner sur toutes sortes d'optimisations courantes du compilateur. Pour un compilateur, les annotations utilisateur de cette nature sont impressionnantes. Le compilateur essaiera de comprendre ce genre de choses, mais il ne le fait généralement pas (la fonction en question peut être dans un autre fichier objet non visible par le compilateur ou utiliser de manière transitoire une fonction qui n'est pas visible), ou quand elle le fait, il y a une exception triviale qui pourrait être levée et dont vous n'êtes même pas au courant, de sorte qu'elle ne peut pas implicitement lanoexcept
nommer (l'allocation de mémoire pourrait lancer bad_alloc, par exemple).la source
x = 5
peut lancer. Si cette partie dutry
bloc servait à quelque chose, le raisonnement ne tiendrait pas.throw
instructions explicites , ou d'autres choses commenew
cela peuvent lancer. Si le compilateur ne peut pas voir le corps, il doit s'appuyer sur la présence ou l'absence denoexcept
. L'accès simple aux tableaux ne génère généralement pas d'exceptions (C ++ n'a pas de vérification des limites), donc non, l'accès aux tableaux ne ferait pas à lui seul penser que la fonction lève des exceptions. (L'accès hors limite est UB, pas une exception garantie.)throw()
en pré-noexcept C ++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 effectuez cette sélection en utilisant la fonctionstd::move_if_noexcept
. Par exemple, la croissance destd::vector
(par exemple, lorsque nous appelonsreserve
) doit fournir une forte garantie d'exception-sécurité. S'il sait queT
le constructeur de mouvement ne lance pas, il peut simplement déplacer chaque élément. Sinon, il doit copier tousT
les par. Cela a été décrit en détail dans cet article .la source
noexcept
y (le cas échéant)! Des fonctions de membre de déplacement définies implicitement y ont éténoexcept
ajoutées automatiquement (le cas échéant).Euh, jamais? N'est-ce jamais un temps? Jamais.
noexcept
est pour les optimisations de performances du compilateur de la même manière queconst
pour les 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 lever une exception. N'oubliez pas: la plupart des compilateurs n'émettent pas de code spécial pour les exceptions à moins qu'il ne lance quelque chose. Ilnoexcept
ne s'agit donc pas de donner au compilateur des conseils sur la façon d'optimiser une fonction autant que de vous donner des conseils sur la façon d'utiliser une fonction.Des modèles comme
move_if_noexcept
détecteront si le constructeur de déplacement est défini avecnoexcept
et renverra unconst&
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ûr de le faire.En général, vous devez utiliser cette option
noexcept
lorsque vous pensez que cela sera réellement utile . Certains codes prendront des chemins différents si celais_nothrow_constructible
est vrai pour ce type. Si vous utilisez du code qui le fera, n'hésitez pas à utilisernoexcept
les constructeurs appropriés.En bref: utilisez-le pour déplacer des constructeurs et des constructions similaires, mais ne vous sentez pas obligé de devenir fou avec.
la source
move_if_noexcept
ne retournera pas de copie, il renverra une const lvalue-reference plutôt qu'une rvalue-reference. En général, l'appelant effectuera une copie au lieu d'un déplacement, maismove_if_noexcept
ne fera pas la copie. Sinon, bonne explication.noexcept
. Alors que "jamais" n'est pas vrai.std::vector
est écrit pour forcer le compilateur à compiler un code différent . Il ne s'agit pas que le compilateur détecte quelque chose; il s'agit du code utilisateur détectant quelque chose.Selon les mots de Bjarne ( The C ++ Programming Language, 4th Edition , page 366):
la source
noexcept
chaque fois, sauf que je veux explicitement m'occuper des exceptions. Soyons réalistes, la plupart des exceptions sont si improbables et / ou si fatales que le sauvetage n'est guère raisonnable ou possible. Par exemple, dans l'exemple cité, si l'allocation échoue, l'application ne pourra guère continuer à fonctionner correctement.noexcept
est délicat, car il fait partie de l'interface des fonctions. Surtout, si vous écrivez une bibliothèque, votre code client peut dépendre de lanoexcept
propriété. Il peut être difficile de le modifier plus tard, car vous pourriez casser le code existant. Cela peut être moins préoccupant 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 elle aimera rester
noexcept
ou cela limitera-t-il les futures implémentations? Par exemple, vous souhaiterez peut-être introduire une vérification d'erreur des arguments illégaux en lançant des exceptions (par exemple, pour les tests unitaires), ou vous pouvez dépendre d'un autre code de bibliothèque qui pourrait changer sa spécification d'exception. Dans ce cas, il est plus sûr d'être conservateur et d'omettrenoexcept
.D'un autre côté, si vous êtes sûr que la fonction ne doit jamais lancer et qu'il est correct qu'elle fasse 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 denoexcept
si votre implémentation change.Il existe quatre classes de fonctions sur lesquelles vous devriez vous concentrer, car elles auront probablement le plus grand impact:
noexcept(true)
sauf si vous les faitesnoexcept(false)
)Ces fonctions devraient généralement l'être
noexcept
, et il est très probable que les implémentations de bibliothèque puissent utiliser lanoexcept
propriété. Par exemple,std::vector
peut utiliser des opérations de déplacement sans lancer sans sacrifier les garanties d'exception fortes. Sinon, il devra se replier sur la copie des éléments (comme il l'a fait en C ++ 98).Ce type d'optimisation est au niveau algorithmique et ne dépend pas des optimisations du compilateur. Cela peut avoir un impact significatif, surtout si les éléments sont coûteux à copier.
L'avantage de
noexcept
contre aucune spécification d'exception outhrow()
est que la norme permet aux compilateurs plus de liberté quand il s'agit de dérouler la pile. Même dans lethrow()
cas, le compilateur doit dérouler complètement la pile (et il doit le faire dans l'ordre inverse exact des constructions d'objet).Dans le
noexcept
cas, en revanche, 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 une optimisation supplémentaire du code car elle réduit la surcharge de toujours pouvoir dérouler la pile.La question connexe à propos de noexcept, du déroulement de la pile et des performances donne plus de détails sur la surcharge lorsque le déroulement de la pile est requis.
Je recommande également le livre de Scott Meyers "Effective Modern C ++", "Item 14: Declare functions noexcept if they don't emit exceptions" pour une lecture plus approfondie.
la source
throws
mot clé au lieu denoexcept
négative. Je ne peux tout simplement pas obtenir certains choix de conception C ++ ...noexcept
parce quethrow
c'était déjà pris. Autrement dit, ilsthrow
peuvent être utilisés presque comme vous le mentionnez, sauf qu'ils ont bâclé la conception de celui-ci, il est donc devenu presque inutile - même nuisible. Mais nous sommes coincés avec cela maintenant car le supprimer serait un changement de rupture avec peu d'avantages. Il ennoexcept
va de même pour l'essentielthrow_v2
.throw
pas utile?throw()
spécificateur d'exception n'a pas fourni la même garantie quenothrow
?Quand vous dites "je sais [qu'ils] ne lanceront jamais", vous voulez dire en examinant l'implémentation de la fonction que vous savez que la fonction ne lancera pas. Je pense que cette approche est à l'envers.
Il vaut mieux se demander si une fonction peut lever des exceptions pour faire partie de la conception de la fonction: aussi importante que la liste d'arguments et si une méthode est un mutateur (...
const
). Déclarer que "cette fonction ne lève jamais d'exceptions" est une contrainte sur l'implémentation. L'omettre ne signifie pas que la fonction peut lever des exceptions; cela signifie que la version actuelle de la fonction et toutes les versions futures peuvent lever 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 donc être appelés à partir de destructeurs, mais aussi pour la mise en œuvre de code de «roll-back» dans des méthodes qui offrent une forte garantie d'exception.la source