51 votes

Le comportement non défini est-il seulement un problème si vous déployez sur plusieurs plateformes ?

La plupart des conversations autour comportement indéfini (UB) parler de la façon dont certaines plateformes peuvent faire ceci, ou certains compilateurs font cela.

Et si vous n'êtes intéressé que par une seule plate-forme et un seul compilateur (même version) et que vous savez que vous allez les utiliser pendant des années ?

Une fois que l'UB s'est manifesté pour cette architecture et ce compilateur et que vous l'avez testé, ne pouvez-vous pas supposer qu'à partir de ce moment-là, ce que le compilateur a fait avec l'UB la première fois, il le fera à chaque fois ?

Note : Je sais. un comportement indéfini est très, très mauvais Mais lorsque j'ai signalé la présence d'UB dans le code écrit par quelqu'un dans cette situation, ils ont posé la question, et je n'ai rien eu de mieux à dire que de dire que si vous deviez un jour mettre à jour ou porter le code, tous les UB seraient très chers à réparer.

Rien ne change sauf le code, et l'UB n'est pas défini par l'implémentation.

56voto

Yakk Points 31636

Les changements de système d'exploitation, les modifications inoffensives du système (une version différente du matériel !) ou les changements de compilateur peuvent tous faire en sorte que l'UB, qui fonctionnait auparavant, ne fonctionne plus.

Mais c'est pire que ça.

Parfois, une modification apportée à une unité de compilation non apparentée, ou à un code éloigné dans la même unité de compilation, peut faire en sorte que l'UB qui fonctionnait auparavant ne fonctionne plus ; par exemple, deux fonctions ou méthodes en ligne avec des définitions différentes mais la même signature. L'une d'entre elles est silencieusement rejetée lors de l'édition de liens, et des modifications de code totalement inoffensives peuvent changer celle qui est rejetée.

Le code qui fonctionne dans un contexte donné peut soudainement cesser de fonctionner dans le même compilateur, le même système d'exploitation et le même matériel lorsque vous l'utilisez dans un contexte différent. Un exemple de ceci est la violation de l'aliasing fort ; le code compilé peut fonctionner lorsqu'il est appelé à l'endroit A, mais lorsqu'il est inlined (éventuellement au moment du lien !) le code peut changer de signification.

Votre code, s'il fait partie d'un projet plus vaste, pourrait appeler conditionnellement du code tiers (disons, une extension shell qui prévisualise un type d'image dans une boîte de dialogue d'ouverture de fichier) qui change l'état de certains drapeaux (précision en virgule flottante, locale, drapeaux de dépassement d'entier, comportement de division par zéro, etc.) Votre code, qui fonctionnait bien auparavant, présente maintenant un comportement complètement différent.

Ensuite, de nombreux types de comportements indéfinis sont intrinsèquement non déterministes. Accéder au contenu d'un pointeur après qu'il ait été libéré (même y écrire) peut être sûr à 99/100, mais à 1/100 la page a été échangée, ou quelque chose d'autre a été écrit avant que vous ne l'atteigniez. Vous avez alors une corruption de la mémoire. Elle passe tous vos tests, mais vous n'aviez pas une connaissance complète de ce qui pouvait se passer.

En utilisant un comportement indéfini, vous vous engagez à comprendre parfaitement la norme C++, tout ce que votre compilateur peut faire dans cette situation et toutes les réactions possibles de l'environnement d'exécution. Vous devez vérifier l'assemblage produit, et non la source C++, éventuellement pour l'ensemble du programme, à chaque fois que vous le construisez ! Vous engagez également toute personne qui lit ce code, ou qui modifie ce code, à ce niveau de connaissance.

Cela en vaut parfois encore la peine.

Délégués les plus rapides possibles utilise l'UB et sa connaissance des conventions d'appel pour être un non propriétaire très rapide. std::function -comme le type.

Des délégués incroyablement rapides est en compétition. Il est plus rapide dans certaines situations, plus lent dans d'autres, et il est conforme à la norme C++.

L'utilisation de l'UB pourrait en valoir la peine, pour l'augmentation des performances. Il est rare que vous gagniez autre chose que les performances (vitesse ou utilisation de la mémoire) en utilisant l'UB.

Un autre exemple que j'ai vu est celui où nous avons dû enregistrer un callback avec une pauvre API C qui ne prenait qu'un pointeur de fonction. Nous créions une fonction (compilée sans optimisation), la copiait sur une autre page, modifiait une constante de pointeur dans cette fonction, puis marquait cette page comme exécutable, ce qui nous permettait de passer secrètement un pointeur avec le pointeur de fonction au callback.

Une autre solution consisterait à disposer d'un ensemble de fonctions de taille fixe (10 ? 100 ? 1000 ? 1 million ?) qui rechercheraient toutes un fichier std::function dans un tableau global et l'invoquer. Cela limite le nombre de callbacks que nous pouvons installer à un moment donné, mais en pratique, c'est suffisant.

20voto

Petr Points 4434

Non, ce n'est pas sûr. Tout d'abord, vous devrez réparer tout et pas seulement la version du compilateur. Je n'ai pas d'exemples particuliers, mais je suppose qu'un système d'exploitation différent (mis à jour), ou même un processeur mis à jour, pourrait modifier les résultats d'UB.

En outre, le simple fait d'introduire des données différentes dans votre programme peut modifier le comportement de l'UB. Par exemple, un accès hors limite à un tableau (du moins sans optimisations) dépend généralement de ce qui se trouve dans la mémoire après le tableau. UPD : voir une excellente réponse de Yakk pour une discussion plus approfondie sur ce sujet.

Un problème plus important est l'optimisation et d'autres drapeaux de compilateur. L'UB peut se manifester de différentes manières selon les drapeaux d'optimisation, et il est assez difficile d'imaginer que quelqu'un utilise toujours les mêmes drapeaux d'optimisation (au moins, vous utiliserez des drapeaux différents pour le débogage et la version).

UPD ) : je viens de remarquer que vous n'avez jamais mentionné la correction d'un compilateur. version vous avez seulement mentionné la réparation d'un compilateur même. Ensuite, tout est encore plus dangereux : les nouvelles versions du compilateur peuvent définitivement changer le comportement de l'UB. À partir de cette série d'articles de blog :

La chose importante et effrayante à réaliser est que juste à peu près tout l'optimisation basée sur un comportement indéfini peut commencer à être déclenchée sur code bogué à tout moment dans le futur. L'internalisation, le déroulement de boucle, la la promotion de la mémoire et d'autres optimisations continueront à s'améliorer, et une et une grande partie de leur raison d'être est d'exposer des optimisations secondaires secondaires comme celles mentionnées ci-dessus.

9voto

Chris Beck Points 830

Il s'agit essentiellement d'une question sur une implémentation spécifique du C++. "Puis-je supposer qu'un comportement spécifique, non défini par la norme, continuera à être traité par ($CXX) sur la plate-forme XYZ de la même manière dans les circonstances UVW ?"

Je pense que vous devriez soit clarifier en disant exactement avec quel compilateur et quelle plateforme vous travaillez, et ensuite consulter leur documentation pour voir s'ils font des garanties, sinon la question est fondamentalement sans réponse.

L'intérêt d'un comportement non défini est que la norme C++ ne spécifie pas ce qui se passe, donc si vous cherchez une sorte de garantie de la norme que c'est "ok", vous ne la trouverez pas. Si vous demandez si la "communauté au sens large" considère que c'est sûr, c'est avant tout une question d'opinion.

Une fois que l'UB s'est manifesté pour cette architecture et ce compilateur et que vous l'avez testé, ne pouvez-vous pas supposer qu'à partir de ce moment-là, ce que le compilateur a fait avec l'UB la première fois, il le fera à chaque fois ?

Seulement si les fabricants de compilateurs garantissent que vous pouvez le faire, sinon, non, c'est un vœu pieux.


Permettez-moi d'essayer de répondre à nouveau d'une manière légèrement différente.

Comme nous le savons tous, dans l'ingénierie logicielle normale, et l'ingénierie en général, on apprend aux programmeurs / ingénieurs à faire les choses selon une norme, les auteurs de compilateurs / fabricants de pièces produisent des pièces / outils qui répondent à une norme, et à la fin vous produisez quelque chose où "sous les hypothèses des normes, mon travail d'ingénierie montre que ce produit fonctionnera", puis vous le testez et l'expédiez.

Supposons que vous ayez un oncle fou, Jimbo, et qu'un jour, il ait sorti tous ses outils et tout un tas de planches de bois, et qu'il ait travaillé pendant des semaines pour fabriquer des montagnes russes dans votre jardin. Et puis tu le fais tourner, et bien sûr il ne s'écrase pas. Et vous l'avez même fait tourner dix fois, et il ne s'est pas écrasé. Maintenant Jimbo n'est pas un ingénieur, donc ce n'est pas fait selon les normes. Mais si elle ne s'est pas écrasée même après dix essais, ça veut dire qu'elle est sûre et que vous pouvez commencer à faire payer l'entrée au public, non ?

Dans une large mesure, ce qui est sûr et ce qui ne l'est pas est une question sociologique. Mais si vous voulez en faire une simple question de "quand puis-je raisonnablement supposer que personne ne sera blessé si je fais payer l'entrée, alors que je ne peux pas vraiment supposer quoi que ce soit sur le produit", voici comment je procéderais. Supposons que j'estime que, si je commence à faire payer l'entrée au public, je l'exploiterai pendant X années, et que pendant cette période, peut-être 100 000 personnes l'utiliseront. S'il s'agit essentiellement d'un tirage au sort biaisé pour savoir s'il se casse ou non, alors ce que je voudrais voir, c'est quelque chose comme "cet appareil a été utilisé un million de fois avec des mannequins de choc, et il ne s'est jamais cassé ou n'a jamais montré de signes de rupture". Je pourrais alors raisonnablement penser que si je commence à faire payer l'entrée au public, les chances que quelqu'un soit blessé sont assez faibles, même si aucune norme technique rigoureuse n'est appliquée. Cela ne serait basé que sur une connaissance générale des statistiques et de la mécanique.

En ce qui concerne votre question, je dirais que si vous expédiez du code avec un comportement non défini, que personne, ni la norme, ni le fabricant du compilateur, ni personne d'autre ne supportera, c'est fondamentalement de l'ingénierie "oncle jimbo fou", et ce n'est "correct" que si vous faites des quantités beaucoup plus importantes de tests pour vérifier qu'il répond à vos besoins, sur la base d'une connaissance générale des statistiques et des ordinateurs.

7voto

petersohn Points 2617

Ce à quoi vous faites référence est plus probable mise en œuvre définie et non comportement indéfini . Dans le premier cas, la norme ne vous dit pas ce qui va se passer mais devrait fonctionner de la même manière si vous utilisez le même compilateur et la même plate-forme. Par exemple, on peut supposer qu'un int a une longueur de 4 octets. UB est quelque chose de plus sérieux. Là, la norme ne dit rien. Il est possible que pour un compilateur et une plate-forme donnés, cela fonctionne, mais il est également possible que cela ne fonctionne que dans certains cas.

L'utilisation de valeurs non initialisées en est un exemple. Si vous utilisez une valeur non initialisée bool dans un if vous pouvez obtenir vrai ou faux, et il se peut que ce soit toujours ce que vous voulez, mais le code se brisera de plusieurs façons surprenantes.

Un autre exemple est le déréférencement d'un pointeur nul. Bien qu'il en résulte probablement un défaut de segmentation dans tous les cas, la norme n'exige pas que le programme produise les mêmes résultats à chaque exécution.

En résumé, si vous faites quelque chose qui est mise en œuvre définie alors vous êtes en sécurité si vous ne développez que pour une seule plate-forme et que vous avez testé son fonctionnement. Si vous faites quelque chose qui est comportement indéfini alors vous n'êtes probablement pas en sécurité dans tous les cas. Il se peut que cela fonctionne, mais rien ne le garantit.

5voto

Cort Ammon Points 1584

Pensez-y d'une manière différente.

Un comportement indéfini est TOUJOURS mauvais et ne devrait jamais être utilisé, car vous ne savez jamais ce que vous obtiendrez.

Cependant, vous pouvez tempérer cela avec

Le comportement peut être défini par d'autres parties que la seule spécification du langage.

Vous ne devez donc jamais vous fier à UB, jamais, mais vous pouvez trouver d'autres sources qui indiquent qu'un certain comportement est un comportement DÉFINI pour votre compilateur dans vos circonstances.

Yakk a donné d'excellents exemples concernant les classes de délégués rapides. Dans ces cas, l'auteur affirme explicitement qu'il s'engage dans un comportement non défini, selon la spécification. Cependant, il explique ensuite une raison commerciale pour laquelle le comportement est mieux défini que cela. Par exemple, il déclare qu'il est peu probable que l'agencement de la mémoire d'un pointeur de fonction membre soit modifié dans Visual Studio, car cela entraînerait des coûts commerciaux considérables dus à des incompatibilités qui déplaisent à Microsoft. Ils déclarent donc que le comportement est "un comportement défini de facto".

Un comportement similaire peut être observé dans l'implémentation linux typique de pthreads (à compiler par gcc). Il y a des cas où ils font des hypothèses sur les optimisations qu'un compilateur est autorisé à invoquer dans des scénarios multithreads. Ces hypothèses sont clairement énoncées dans les commentaires du code source. En quoi s'agit-il d'un "comportement défini de facto" ? Eh bien, pthreads et gcc vont en quelque sorte main dans la main. Il serait considéré comme inacceptable d'ajouter une optimisation à gcc qui casserait pthreads, donc personne ne le fera jamais.

Cependant, vous ne pouvez pas faire la même supposition. Vous pouvez dire "pthreads le fait, donc je devrais pouvoir le faire aussi". Ensuite, quelqu'un fait une optimisation et met à jour gcc pour qu'il fonctionne avec elle (peut-être en utilisant la fonction __sync au lieu de s'appuyer sur volatile ). Maintenant pthreads continue à fonctionner... mais votre code ne fonctionne plus.

Prenons également le cas de MySQL (ou Postgre ?), qui a découvert une erreur de dépassement de tampon. Le dépassement avait en fait été détecté dans le code, mais en utilisant un comportement non défini, de sorte que la dernière version de gcc a commencé à optimiser l'ensemble du contrôle.

Donc, dans l'ensemble, cherchez une autre source pour définir le comportement, plutôt que de l'utiliser alors qu'il n'est pas défini. Il est tout à fait légitime de trouver une raison pour laquelle vous savez que 1,0/0,0 est égal à NaN, plutôt que de provoquer un piège en virgule flottante. Mais n'utilisez jamais cette hypothèse sans avoir d'abord prouvé qu'il s'agit d'une définition de comportement valide pour vous et votre compilateur.

Et s'il vous plaît, rappelez-vous que nous mettons à jour les compilateurs de temps en temps.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X