La philosophie derrière le comportement indéfini

59

Les spécifications C \ C ++ laissent un grand nombre de comportements que les compilateurs peuvent implémenter à leur manière. Il y a un certain nombre de questions qui sont toujours posées ici à propos de la même chose et nous avons d'excellents articles à ce sujet:

Ma question ne concerne pas ce qu'est un comportement indéfini, ou est-ce vraiment mauvais. Je connais les dangers et la plupart des citations pertinentes sur le comportement non défini de la norme. Veuillez vous abstenir de poster des réponses concernant son état déplorable. Cette question concerne la philosophie qui sous-tend de supprimer autant de comportements ouverts à la mise en œuvre du compilateur.

J'ai lu un excellent article de blog qui indique que la performance est la raison principale. Je me demandais si la performance était le seul critère permettant de le permettre, ou existe-t-il d'autres facteurs qui influencent la décision de laisser les choses ouvertes pour la mise en œuvre du compilateur?

Si vous avez des exemples à citer sur la manière dont un comportement non défini particulier fournit suffisamment d'espace pour que le compilateur puisse les optimiser, veuillez les énumérer. Si vous connaissez des facteurs autres que les performances, veuillez fournir une réponse suffisamment détaillée.

Si vous ne comprenez pas la question ou si vous ne disposez pas de suffisamment de preuves / de sources pour appuyer votre réponse, veuillez ne pas publier de réponses spéculatives à grande échelle.

Alok Sauvegarder
la source
7
qui a jamais entendu parler d'un ordinateur déterministe de toute façon?
Sova
1
comme le dit l'excellente réponse de litb, programmers.stackexchange.com/a/99741/192238 , le titre et le corps de cette question semblent un peu incohérents: "les comportements que les compilateurs peuvent implémenter à leur manière" sont généralement qualifiés de définis par l' implémentation . Bien sûr, l'auteur de l'implémentation peut définir l'UB réelle, mais le plus souvent, cela ne le dérange pas (et l'optimise au maximum, etc.)
underscore_d
Quelque chose de semblable à ceci softwareengineering.stackexchange.com/questions/398703/…
Sisir

Réponses:

49

Premièrement, je noterai que bien que je ne mentionne que "C", la même chose s’applique également au C ++.

Le commentaire mentionnant Godel était en partie (mais seulement en partie) pertinent.

Lorsque vous descendez à elle, un comportement non défini dans les normes C est en grande partie tout en soulignant la limite entre ce que les tentatives standards pour définir, et ce qu'il ne fonctionne pas.

Les théorèmes de Godel (il y en a deux) disent qu'il est impossible de définir un système mathématique qui peut être prouvé (par ses propres règles) à la fois complet et cohérent. Vous pouvez faire vos règles pour qu'elles soient complètes (le cas dont il s'est occupé était les règles "normales" pour les nombres naturels), ou bien vous pouvez permettre de prouver sa cohérence, mais vous ne pouvez pas avoir les deux.

Dans le cas de quelque chose comme C, cela ne s'applique pas directement - pour la plupart, la "prouvabilité" de l'exhaustivité ou de la cohérence du système n'est pas une priorité absolue pour la plupart des concepteurs de langage. En même temps, oui, ils ont probablement été influencés (au moins dans une certaine mesure) en sachant qu'il est impossible de définir un système «parfait» - un système parfaitement complet et cohérent. Sachant qu'une telle chose est impossible a peut-être rendu un peu plus facile de prendre du recul, de respirer un peu et de décider des limites de ce qu'ils essaient de définir.

Au risque d'être (encore une fois) accusé d'arrogance, je qualifierais le standard C de régi (en partie) par deux idées de base:

  1. Le langage doit prendre en charge une variété de matériels aussi large que possible (idéalement, tout le matériel "sain" doit être abaissé à une limite inférieure raisonnable).
  2. Le langage doit permettre d’écrire le plus grand nombre possible de logiciels pour l’environnement donné.

La première signifie que si quelqu'un définit un nouveau processeur, il devrait être possible de fournir une bonne implémentation de C, solide et utilisable, à condition que sa conception se rapproche au moins raisonnablement de quelques directives simples - en gros, si suit quelque chose sur l'ordre général du modèle de Von Neumann et fournit au moins une quantité de mémoire minimale raisonnable, qui devrait être suffisante pour permettre une implémentation en C. Pour une implémentation "hébergée" (fonctionnant sur un système d'exploitation), vous devez prendre en charge une notion qui correspond de manière assez proche aux fichiers et qui a un jeu de caractères avec un certain jeu minimal de caractères (91 requis).

Le second moyen , il devrait être possible d'écrire du code qui manipule le matériel directement, de sorte que vous pouvez écrire des choses comme les chargeurs de démarrage, les systèmes d' exploitation, logiciels embarqués qui fonctionne sans système d' exploitation, etc. Il y a en fin de compte des limites à cet égard, donc presque tout système d’exploitation pratique, chargeur de démarrage, etc., est susceptible de contenir au moins un peu de code écrit en langage assembleur. De même, même un petit système intégré est susceptible d'inclure au moins une sorte de routine de bibliothèque pré-écrite pour donner accès aux périphériques sur le système hôte. Bien qu’il soit difficile de définir une limite précise, l’intention est que la dépendance à ce code soit réduite au minimum.

Le comportement non défini dans le langage est en grande partie motivé par l'intention du langage de prendre en charge ces capacités. Par exemple, le langage vous permet de convertir un entier arbitraire en pointeur et d'accéder à tout ce qui se trouve à cette adresse. La norme n'essaie pas de dire ce qui se passera quand vous le ferez (par exemple, même la lecture de certaines adresses peut avoir des effets visibles de l'extérieur). En même temps, il ne fait aucun effort pour vous empêcher de faire de telles choses, car vous devez utiliser certains types de logiciels que vous êtes censé être capable d’écrire en C.

Il existe également un comportement indéfini lié à d'autres éléments de conception. Par exemple, une autre intention de C est de prendre en charge une compilation séparée. Cela signifie (par exemple) qu'il est prévu que vous puissiez "relier" des morceaux en utilisant un éditeur de liens qui suit à peu près ce que la plupart d'entre nous voyons comme le modèle habituel d'un éditeur de liens. En particulier, il devrait être possible de combiner des modules compilés séparément dans un programme complet sans connaissance de la sémantique du langage.

Il existe un autre type de comportement indéfini (qui est beaucoup plus courant en C ++ qu'en C) et qui existe simplement à cause des limites de la technologie du compilateur - des choses que nous connaissons fondamentalement sont des erreurs et que nous voudrions probablement que le compilateur diagnostique comme telles, mais étant donné les limites actuelles de la technologie des compilateurs, il est douteux qu’elles puissent être diagnostiquées en toutes circonstances. Bon nombre d’entre elles sont dictées par d’autres exigences, telles que la compilation séparée, il s’agit donc en grande partie d’équilibrer des exigences contradictoires. Dans ce cas, le comité a généralement opté pour une plus grande capacité, même si cela ne permet pas de diagnostiquer certains problèmes éventuels, plutôt que de limiter les capacités pour assurer que tous les problèmes possibles sont diagnostiqués.

Ces différences d' intention expliquent la plupart des différences entre C et quelque chose comme Java ou les systèmes basés sur CLI de Microsoft. Ces derniers se limitent assez explicitement à travailler avec un ensemble de matériel beaucoup plus limité ou à obliger les logiciels à émuler le matériel plus spécifique qu'ils ciblent. Ils ont également pour objectif spécifique d’ empêcher toute manipulation directe du matériel, mais vous obligent plutôt à utiliser quelque chose comme JNI ou P / Invoke (et un code écrit en quelque chose comme C) pour même tenter une telle tentative.

Pour revenir un instant aux théorèmes de Godel, nous pouvons faire un parallèle: Java et CLI ont opté pour l’alternative "cohérente en interne", tandis que C a opté pour l’alternative "complète". Bien sûr, cela est une analogie très rude - je doute que quiconque de tenter une preuve formelle de soit la cohérence interne ou l' exhaustivité dans les deux cas. Néanmoins, la notion générale correspond assez étroitement aux choix qu’ils ont pris.

Jerry Coffin
la source
25
Je pense que les théorèmes de Godel sont un fouillis rouge. Ils traitent de la démonstration d'un système à partir de ses propres axiomes, ce qui n'est pas le cas ici: C n'a pas besoin d'être spécifié en C. Il est tout à fait possible d'avoir un langage complètement spécifié (considérons une machine de Turing).
poolie
9
Désolé, mais je crains que vous n'ayez complètement mal compris les théorèmes de Godel. Ils traitent de l'impossibilité de prouver toutes les déclarations vraies dans un système logique cohérent; En termes de calcul, le théorème d'incomplétude est analogue à dire qu'il existe des problèmes qui ne peuvent être résolus par aucun programme - les problèmes sont analogues aux déclarations vraies, des programmes à des preuves et au modèle de calcul du système logique. Cela n'a aucun lien avec un comportement indéfini. Voir une explication de l'analogie ici: scottaaronson.com/blog/?p=710 .
Alex ten Brink
5
Je dois noter qu’une machine Von Neumann n’est pas requise pour une implémentation en C. Il est parfaitement possible (et même pas très difficile) de développer une implémentation C pour une architecture de Harvard (et je ne serais pas surpris de voir autant d'implémentations de ce type sur des systèmes embarqués)
bdonlan
1
Malheureusement, la philosophie du compilateur C moderne amène UB à un tout autre niveau. Même dans les cas où un programme était prêt à traiter presque toutes les conséquences "naturelles" plausibles d'une forme particulière de comportement indéfini, et que celles qu'il ne pouvait pas traiter seraient au moins reconnaissables (par exemple, un dépassement d'entier piégé), la nouvelle philosophie favorise contournant tout code qui ne pourrait pas être exécuté à moins que UB ne se produise, transformant un code qui se serait comporté correctement pour la plupart des implémentations en un code "plus efficace" mais tout simplement faux.
Supercat
20

La raison C explique

Les termes comportement non spécifié, comportement non défini et comportement défini par l'implémentation sont utilisés pour classer le résultat d'écriture de programmes dont les propriétés ne sont pas ou ne peuvent pas être décrites en détail par la norme. L’adoption de cette catégorisation a pour objectif de permettre une certaine variété parmi les implémentations, ce qui permet à la qualité de l’implémentation de jouer un rôle actif sur le marché, ainsi que certaines extensions populaires sans supprimer le cachet de la conformité à la norme. L'annexe F de la norme répertorie les comportements relevant de l'une de ces trois catégories.

Un comportement non spécifié laisse au réalisateur une certaine latitude pour traduire les programmes. Cette latitude ne va pas jusqu'à échouer dans la traduction du programme.

Un comportement non défini permet à l'implémenteur de ne pas intercepter certaines erreurs de programme difficiles à diagnostiquer. Il identifie également les zones d'extension de langage conforme possible: l'implémenteur peut augmenter le langage en fournissant une définition du comportement officiellement indéfini.

Le comportement défini par la mise en œuvre donne à un implémenteur la liberté de choisir l'approche appropriée, mais nécessite que ce choix soit expliqué à l'utilisateur. Les comportements désignés comme définis par l'implémentation sont généralement ceux dans lesquels un utilisateur peut prendre des décisions de codage significatives basées sur la définition d'implémentation. Les responsables de la mise en œuvre doivent garder à l'esprit ce critère lorsqu'ils déterminent l'étendue d'une définition de mise en œuvre. Comme pour le comportement non spécifié, le fait de ne pas traduire le code source contenant le comportement défini par l'implémentation ne constitue pas une réponse adéquate.

Les avantages pour les programmes sont également importants, pas seulement pour les implémentations. Un programme qui dépend d'un comportement indéfini peut toujours être conforme s'il est accepté par une implémentation conforme. L'existence d'un comportement non défini permet à un programme d'utiliser des caractéristiques non portables explicitement marquées comme telles ("comportement non défini"), sans pour autant devenir non conforme. Les notes de justification:

Le code C peut être non portable. Bien qu'il se soit efforcé de donner aux programmeurs la possibilité d'écrire de véritables programmes portables, le Comité n'a pas voulu contraindre les programmeurs à écrire de manière portable, afin d'empêcher l'utilisation de C en tant qu '"assembleur de haut niveau": le code est l’un des atouts de C. C’est ce principe qui motive en grande partie la distinction entre programme strictement conforme et programme conforme (§1.7).

Et à 1,7 il note

La triple définition de la conformité est utilisée pour élargir la population de programmes conformes et faire la distinction entre les programmes conformes utilisant une seule implémentation et les programmes portables compatibles.

Un programme strictement conforme est un autre terme pour un programme extrêmement portable. L’objectif est de donner au programmeur une chance de créer des programmes C puissants et hautement portables, sans déprécier les programmes C parfaitement utiles qui ne sont pas portables. Ainsi l'adverbe strictement.

Ainsi, ce petit programme sale qui fonctionne parfaitement sur GCC est toujours conforme !

Johannes Schaub - litb
la source
15

La rapidité est particulièrement problématique par rapport à C. Si C ++ faisait certaines choses sensées, telles que l’initialisation de grands tableaux de types primitifs, il perdrait une tonne de points de repère au code C. Donc, C ++ initialise ses propres types de données, mais laisse les types C tels quels.

Les autres comportements indéfinis ne font que refléter la réalité. Un exemple est le transfert de bits avec un nombre plus grand que le type. Cela diffère réellement entre les générations de matériel de la même famille. Si vous avez une application 16 bits, le même binaire exact donnera des résultats différents sur un 80286 et un 80386. Le langage standard dit donc que nous ne savons pas!

Certains éléments sont simplement conservés tels qu’ils étaient, comme l’ordre d’évaluation des sous-expressions n’est pas spécifié. A l'origine, on pensait que cela aiderait les rédacteurs de compilateur à optimiser leurs performances. De nos jours, les compilateurs sont assez bons pour le comprendre, mais le coût de trouver toutes les places disponibles dans les compilateurs existants qui profitent de la liberté est tout simplement trop élevé.

Bo Persson
la source
+1 pour le deuxième paragraphe, qui montre quelque chose qu'il serait difficile de spécifier comme comportement défini par l'implémentation.
David Thornley
3
Le décalage de bit est juste un exemple d'acceptation d'un comportement non défini du compilateur et d'utilisation des capacités matérielles. Il serait trivial de spécifier un résultat C pour un décalage de bit lorsque le nombre est supérieur au type, mais coûteux à implémenter sur certains matériels.
mattnz
7

À titre d'exemple, les accès de pointeur doivent presque toujours être indéfinis et pas nécessairement pour des raisons de performances. Par exemple, sur certains systèmes, le chargement de registres spécifiques avec un pointeur générera une exception matérielle. Sous SPARC, l’accès à un objet mémoire mal aligné provoquera une erreur de bus, mais sous x86, il serait "simplement" lent. Il est difficile de spécifier le comportement dans ces cas, car le matériel sous-jacent dicte ce qui va se passer et le C ++ est portable pour de nombreux types de matériel.

Bien sûr, cela donne également au compilateur la liberté d'utiliser des connaissances spécifiques à l'architecture. Pour un exemple de comportement non spécifié, le décalage à droite des valeurs signées peut être logique ou arithmétique, en fonction du matériel sous-jacent, afin de permettre l'utilisation de l'opération de décalage disponible et de ne pas forcer son émulation logicielle.

Je pense aussi que cela rend le travail du compilateur-écrivain plutôt facile, mais je ne me souviens pas de l'exemple pour l'instant. Je l'ajouterai si je me souviens de la situation.

Mark B
la source
3
Le langage C aurait pu être spécifié de telle sorte qu'il devait toujours utiliser des lectures octet par octet sur des systèmes soumis à des restrictions d'alignement et qu'il devait fournir des interruptions d'exception avec un comportement bien défini pour les accès non valides. Mais bien sûr, tout cela aurait été incroyablement coûteux (en taille de code, en complexité et en performances) et n'aurait offert aucun avantage à un code sain et correct.
R ..
6

Simple: rapidité et portabilité. Si C ++ garantit que vous obtenez une exception lorsque vous dé-référencez un pointeur non valide, il ne sera pas portable vers un matériel intégré. Si le C ++ garantissait d'autres choses, comme toujours les primitives initialisées, alors ce serait plus lent, et à l'époque de l'origine du C ++, le ralentissement était une très mauvaise chose.

DeadMG
la source
1
Hein? Qu'est-ce que les exceptions ont à voir avec le matériel embarqué?
Mason Wheeler
2
Les exceptions peuvent verrouiller le système de manière très néfaste pour les systèmes embarqués qui doivent réagir rapidement. Il existe des situations dans lesquelles une lecture erronée est beaucoup moins dommageable qu'un système ralenti.
Ingénieur du monde
1
@Mason: Parce que le matériel doit récupérer l'accès invalide. Il est facile pour Windows de générer une violation d'accès et plus difficile pour le matériel intégré sans système d'exploitation de faire quoi que ce soit, sauf de mourir.
DeadMG
3
Rappelez-vous également que chaque CPU ne dispose pas d'une MMU pour se protéger des accès non valides dans le matériel. Si vous commencez à demander à votre langue de vérifier tous les accès au pointeur, vous devez émuler une MMU sur des processeurs sans ordinateur - et TOUT accès à la mémoire devient donc extrêmement coûteux.
moelleux
4

C a été inventé sur une machine avec des octets de 9 bits et aucune unité à virgule flottante - supposons qu’il soit obligatoire que les octets soient de 9 bits, les mots de 18 bits et que les flotteurs soient implémentés à l’aide de pré-IEEE754 aritmatic?

Martin Beckett
la source
5
Je suppose que vous songez à Unix - C a été utilisé à l’origine sur le PDP-11, qui était en fait assez classique. Je pense que l'idée de base est néanmoins valable.
Jerry Coffin
@ Jerry - oui, tu as raison - je vieillis!
Martin Beckett
Ouais - arrive au meilleur de nous, j'ai peur.
Jerry Coffin
4

Je ne pense pas que la première raison pour UB était de laisser de la place au compilateur pour l'optimiser, mais simplement la possibilité d'utiliser l'implémentation évidente pour les cibles à un moment où les architectures étaient plus variées que maintenant (rappelez-vous si C a été conçu sur une PDP-11 qui a une architecture quelque peu familière, le premier port était celui de Honeywell 635 qui est beaucoup moins familier - mot adressable, mots de 36 bits, octets de 6 ou 9 bits, adresses de 18 bits ... enfin au moins, il utilisait des 2 complément). Mais si l'optimisation lourde n'était pas une cible, l'implémentation évidente n'inclut pas l'ajout de vérifications d'exécution en cas de dépassement de capacité, le nombre de décalages par rapport à la taille du registre, les alias dans les expressions modifiant plusieurs valeurs.

Une autre chose prise en compte était la facilité de mise en œuvre. À l'époque, le compilateur AC comportait plusieurs passes utilisant plusieurs processus, car il n'aurait pas été possible de gérer tout ce processus (le programme aurait été trop volumineux). Il était difficile de demander une vérification de cohérence poussée - en particulier lorsque plusieurs UC étaient impliquées. (Un autre programme que les compilateurs C, lint, a été utilisé pour cela).

Programmateur
la source
Je me demande ce qui a poussé la philosophie changeante d'UB de "Autoriser les programmeurs à utiliser les comportements exposés par leur plate-forme" à "Trouver des excuses pour permettre aux compilateurs d'implémenter un comportement totalement farfelu"? Je me demande également dans quelle mesure ces optimisations finissent par améliorer la taille du code une fois que le code a été modifié pour fonctionner sous le nouveau compilateur? Je ne serais pas surpris que dans de nombreux cas, l'ajout de telles "optimisations" au compilateur ait pour seul effet de forcer les programmeurs à écrire un code plus volumineux et plus lent, de manière à éviter que le compilateur le casse.
Supercat
C'est une dérive en POV. Les gens sont devenus moins conscients de la machine sur laquelle leur programme est exécuté, ils se sont davantage préoccupés de la portabilité et ont donc évité de dépendre d'un comportement non défini, non spécifié et défini par la mise en œuvre. Les optimiseurs ont subi des pressions pour obtenir les meilleurs résultats possibles, ce qui implique d'utiliser toutes les clémences laissées par les spécifications des langues. Il y a aussi le fait qu'Internet - Usenet à la fois, SE de nos jours - les avocats spécialisés dans le langage ont également tendance à donner une vision partiale de la raison sous-jacente et du comportement des rédacteurs du compilateur.
AProgrammer
1
Ce que je trouve curieux, ce sont les affirmations que j'ai vues sur l'effet de "C suppose que les programmeurs ne se livreront jamais à un comportement indéfini" - un fait qui, historiquement, n'était pas vrai. Une déclaration correcte aurait été "C supposé que les programmeurs ne déclencheraient pas un comportement non défini par la norme à moins d’être préparé à faire face aux conséquences naturelles de ce comportement sur la plate-forme . C étant conçu comme un langage de programmation système, une grande partie de son objectif est: était de permettre aux programmeurs de faire des choses spécifiques au système qui ne sont pas définies par la norme de langage; l'idée qu'ils ne le fassent jamais est absurde.
supercat
Il est bon que les programmeurs déploient des efforts supplémentaires pour garantir la portabilité dans les cas où différentes plates-formes auraient des tâches différentes , mais les rédacteurs de compilateurs perdent du temps à éliminer les comportements que les programmeurs auraient normalement pu raisonnablement espérer être communs à tous les compilateurs futurs. Étant donné les nombres entiers iet n, de sorte que n < INT_BITSet i*(1<<n)ne déborderait pas, je considérerais i<<=n;être plus clair que i=(unsigned)i << n;; sur de nombreuses plateformes, il serait plus rapide et plus petit que i*=(1<<N);. Que gagne-t-on en interdisant les compilateurs?
Supercat
Bien que je pense qu’il serait bien que la norme autorise les pièges pour de nombreuses choses qu’elle appelle UB (par exemple, un dépassement d’entier), et il ya de bonnes raisons de ne pas exiger que les pièges fassent quoi que ce soit qui soit prévisible, mais je pense que, à tous points de vue, le La norme serait améliorée si la plupart des formes de papier UB devaient être soit indéterminées, soit documentées, mais elles se réservaient le droit de faire autre chose sans qu'il soit absolument nécessaire de documenter ce qu'elles pourraient être. Les compilateurs qui ont tout fait "UB" seraient légaux, mais probablement défavorisés ...
Supercat
3

L'un des premiers cas classiques était l'addition d'entiers signés. Sur certains des processeurs utilisés, cela causerait une erreur, et sur d'autres, il continuerait simplement avec une valeur (probablement la valeur modulaire appropriée). Spécifier l'un ou l'autre cas signifierait que les programmes pour les machines avec le style arithmétique défavorisé devraient avoir un code supplémentaire, y compris une branche conditionnelle, pour quelque chose d'aussi similaire que l'addition d'un entier.

David Thornley
la source
L'addition d'entiers est un cas intéressant. au-delà de la possibilité d'un comportement d'interruption qui, dans certains cas, serait utile mais pourrait dans d'autres cas entraîner une exécution aléatoire du code, il est parfois raisonnable qu'un compilateur fasse des déductions en se basant sur le fait que le dépassement d'entier n'est pas spécifié pour être encapsulé. Par exemple, un compilateur où la résolution intest de 16 bits et où les décalages étendus de signes sont coûteux pourrait être calculé à l' (uchar1*uchar2) >> 4aide d'un décalage non étendu de signes. Malheureusement, certains compilateurs étendent les inférences non seulement aux résultats, mais également aux opérandes.
Supercat
2

Je dirais que c’était moins de la philosophie que de la réalité - C a toujours été un langage multi-plateforme, et la norme doit en tenir compte et le fait qu’à la publication de toute norme, il y aura une grand nombre d'implémentations sur de nombreux matériels différents. Une norme interdisant les comportements nécessaires serait soit ignorée, soit créée par un organisme de normalisation concurrent.

jmoreno
la source
À l'origine, de nombreux comportements n'étaient pas définis pour permettre à différents systèmes de faire des choses différentes, y compris déclencher une interruption matérielle avec un gestionnaire pouvant ou non être configurable (et risquant, à défaut, de provoquer un comportement arbitrairement imprévisible). Exiger qu'un décalage à gauche d'une valeur négative, pas un piège, par exemple, casse tout code conçu pour un système où il fonctionne et repose sur un tel comportement. En bref, ils ont été laissés indéfinis afin de ne pas empêcher les implémenteurs de fournir des comportements qu'ils jugeaient utiles .
Supercat
Malheureusement, cependant, cela a été tordu de telle sorte que même le code qui sait qu'il tourne sur un processeur qui ferait quelque chose d'utile dans un cas particulier ne peut tirer parti d'un tel comportement, car les compilateurs peuvent utiliser le fait que la norme C ne 'ne spécifiez pas le comportement (bien que ce soit le cas de la plate-forme) d'appliquer des réécritures bizarro-world au code.
Supercat
1

Certains comportements ne peuvent être définis par aucun moyen raisonnable. Je veux dire accéder à un pointeur supprimé. Le seul moyen de le détecter serait d’interdire la valeur du pointeur après la suppression (mémoriser sa valeur quelque part et ne plus permettre à une fonction d’allocation de la renvoyer). Non seulement une telle mémorisation serait excessive, mais, pour un programme de longue durée, les valeurs de pointeurs autorisées seraient épuisées.

Tadeusz Kopec
la source
ou vous pouvez allouer tous les pointeurs au fur weak_ptret à mesure et annuler toutes les références à un pointeur qui se deleted ... oh, attendez, nous approchons du ramassage des ordures: /
Matthieu M. Le
boost::weak_ptrL'implémentation de est un très bon modèle pour commencer pour ce modèle d'utilisation. Plutôt que de suivre et d’annuler de manière weak_ptrsexterne, un weak_ptrsimple contribue au shared_ptrcompte faible de, et le compte faible est fondamentalement un refcount du pointeur lui-même. Ainsi, vous pouvez annuler le shared_ptrsans avoir à le supprimer immédiatement. Ce n’est pas parfait (il est toujours possible que de nombreux expirés weak_ptrconservent le sous-jacent shared_countsans raison valable), mais au moins ils sont rapides et efficaces.
moelleux
0

Je vais vous donner un exemple où il n’ya quasiment pas de choix sensé autre que le comportement indéfini. En principe, tout pointeur pourrait pointer sur la mémoire contenant une variable, à l'exception des variables locales que le compilateur sait ne jamais avoir eu leur adresse prise. Cependant, pour obtenir des performances acceptables sur un processeur moderne, un compilateur doit copier les valeurs de variable dans des registres. Exploiter entièrement la mémoire est un non-démarreur.

Cela vous donne essentiellement deux choix:

1) Tout effacer des registres avant tout accès via un pointeur, juste au cas où le pointeur pointe vers la mémoire de cette variable particulière. Puis chargez tout ce qui est nécessaire dans le registre, juste au cas où les valeurs auraient été modifiées via le pointeur.

2) Avoir un ensemble de règles pour lorsqu'un pointeur est autorisé à aliaser une variable et lorsque le compilateur est autorisé à supposer qu'un pointeur n'alias pas une variable.

C opte pour l’option 2, car 1 serait terrible pour la performance. Mais alors, que se passe-t-il si un pointeur alias une variable de la manière que les règles C interdisent? Etant donné que l'effet dépend de si le compilateur a effectivement enregistré la variable dans un registre, le standard C n'a aucun moyen de garantir de manière définitive des résultats spécifiques.

David Schwartz
la source
Il y aurait une différence sémantique entre dire "Un compilateur est autorisé à se comporter comme si X est vrai" et dire "Tout programme où X n'est pas vrai se livrera à un comportement indéfini", bien que les normes ne permettent malheureusement pas de faire la distinction. Dans de nombreuses situations, y compris votre exemple de repliement du spectre, l'ancienne instruction autorisait de nombreuses optimisations du compilateur qui seraient impossibles autrement. ce dernier permet quelques "optimisations" supplémentaires, mais beaucoup de ces dernières sont des choses que les programmeurs ne voudraient pas.
Supercat
Par exemple, si un code définit a foosur 42, puis appelle une méthode qui utilise un pointeur modifié illégitimement pour définir foosur 44, je vois un avantage à dire que, jusqu'à la prochaine écriture "légitime" foo, des tentatives de lecture peuvent légitimement donnez 42 ou 44, et une expression comme foo+foopourrait donner 86, mais je vois moins d'avantages à permettre au compilateur de faire des inférences étendues et même rétroactives, modifiant ainsi le comportement indéfini dont les comportements "naturels" plausibles auraient été bienveillants. générer du code insensé.
Supercat
0

Historiquement, le comportement non défini avait deux objectifs principaux:

  1. Pour éviter de demander aux auteurs du compilateur de générer du code afin de gérer des conditions qui n'étaient jamais supposées se produire.

  2. Pour permettre la possibilité qu'en l'absence de code traitant explicitement de telles conditions, les implémentations peuvent avoir différents types de comportements "naturels" qui pourraient, dans certains cas, être utiles.

À titre d’exemple simple, sur certaines plates-formes matérielles, la tentative d’addition de deux entiers signés positifs dont la somme est trop importante pour tenir dans un entier signé aboutira à un entier signé négatif. Sur d'autres implémentations, il déclenchera une interruption du processeur. Pour que la norme C oblige l'un ou l'autre comportement, les compilateurs de plateformes dont le comportement naturel diffère de la norme devraient générer du code supplémentaire pour obtenir le comportement correct - un code qui peut être plus coûteux que le code pour effectuer l'addition réelle. Pire, cela voudrait dire que les programmeurs qui souhaitaient le comportement «naturel» devraient ajouter encore plus de code pour le réaliser (et que le code supplémentaire serait encore plus coûteux que l’ajout).

Malheureusement, certains auteurs de compilateurs ont adopté la philosophie voulant que les compilateurs s’efforcent de trouver des conditions susceptibles d’évoquer un comportement indéfini et, en supposant que de telles situations ne se reproduisent jamais, en tirent des conclusions étendues. Ainsi, sur un système avec 32 bits int, un code donné tel que:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

La norme C permettrait au compilateur de dire que si q est égal à 46341 ou plus, l’expression q * q donnera un résultat trop grand pour tenir dans un intcomportement indéfini , ce qui entraînera le comportement indéfini du compilateur. ne peut pas arriver et donc ne serait pas tenu d'incrémenter *psi cela se produit. Si le code appelant utilise *pcomme indicateur l’annulation des résultats du calcul, l’optimisation peut avoir pour effet de prendre du code qui aurait donné des résultats sensibles sur des systèmes fonctionnant de la manière la plus imaginable avec débordement d’entier (le recouvrement peut être moche, mais serait au moins raisonnable), et l'a transformé en code qui peut se comporter de façon absurde.

supercat
la source
-6

L’efficacité est le prétexte habituel, mais quel que soit le prétexte, un comportement indéfini est une idée terrible pour la portabilité. En réalité, les comportements non définis deviennent des hypothèses non vérifiées et non vérifiées.

Ddyer
la source
7
Le PO a précisé ceci: "Ma question ne porte pas sur ce qu'est un comportement indéfini, ou est-il vraiment mauvais. Je connais les dangers et la plupart des citations pertinentes sur le comportement non défini définies dans la norme; évitez donc de poster des réponses concernant leur gravité. . " On dirait que vous n'avez pas lu la question.
Etienne de Martel