102 votes

Le "comportement indéfini" permet-il vraiment à n'importe quoi de se produire ?

L'exemple apocryphe classique de "comportement non défini" est, bien sûr, celui des "démons nasaux" - une impossibilité physique, indépendamment de ce que les normes C et C++ autorisent.

Parce que les communautés C et C++ ont tendance à mettre l'accent sur l'imprévisibilité du comportement non défini et sur l'idée que le compilateur est autorisé à faire faire au programme des choses littérales tout ce qui est lorsque l'on rencontre un comportement non défini, j'avais supposé que la norme n'imposait aucune restriction sur le comportement de, eh bien, le comportement non défini.

Mais le La citation pertinente de la norme C++ semble être la suivante :

[C++14: defns.undefined]: [..] Le comportement non défini autorisé va de l'ignorance totale de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée et caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), en passant par l'interruption de la traduction ou de l'exécution (avec émission d'un message de diagnostic). [..]

Il s'agit en fait d'un petit ensemble d'options possibles :

  • Ignorer la situation -- Oui, la norme poursuit en disant que cela aura des "résultats imprévisibles", mais ce n'est pas la même chose que le compilateur en insérant (ce qui, je suppose, serait un prérequis pour, vous savez, les démons nasaux).
  • Se comporter d'une manière documentée et caractéristique de l'environnement -- cela semble en fait relativement bénin. (Je n'ai certainement pas entendu parler de cas documentés de démons nasaux).
  • Interrompre la traduction ou l'exécution -- avec un diagnostic, pas moins. Si seulement toutes les UB se comportaient aussi bien.

Je suppose que dans la plupart des cas, les compilateurs choisissent d'ignorer le comportement non défini ; par exemple, lors de la lecture d'une mémoire non initialisée, il serait vraisemblablement anti-optimisation d'insérer un quelconque code pour assurer un comportement cohérent. Je suppose que les types plus étranges de comportement non défini (tels que " voyage dans le temps ") relèverait de la deuxième catégorie - mais cela exige que ces comportements soient documentés et " caractéristiques de l'environnement " (donc je suppose que les démons nasaux ne sont produits que par des ordinateurs infernaux ?).

Ai-je mal compris la définition ? S'agit-il de simples exemples de ce qui pourrait constituer un comportement non défini, plutôt qu'une liste complète d'options ? L'affirmation selon laquelle "tout peut arriver" est-elle simplement considérée comme un effet secondaire inattendu de l'ignorance de la situation ?

Deux petits points de clarification :

  • Je pensais que la question initiale était claire, et je pense que c'est le cas pour la plupart des gens, mais je vais quand même l'expliquer : Je réalise que "démons nasaux" est une plaisanterie.
  • Veuillez ne pas écrire une (autre) réponse expliquant que UB permet des optimisations de compilateur spécifiques à la plate-forme, à moins que vous ne également expliquer comment il permet des optimisations qui définie par la mise en œuvre comportement ne serait pas permettre.

Cette question n'était pas destinée à servir de forum de discussion sur les (dé)mérites d'un comportement indéfini, mais c'est un peu ce qu'elle est devenue. Dans tous les cas, ce fil de discussion à propos d'un hypothétique compilateur C sans comportement indéfini peut présenter un intérêt supplémentaire pour ceux qui pensent que ce sujet est important.

3 votes

Il s'agit en fait de différences entre les systèmes d'exploitation. Par exemple, la mémoire est-elle initialisée à zéro ? Y a-t-il un gardien de pile actif ? Utilise-t-il la randomisation de l'adresse ? La spécification est silencieuse car différents comportements sont possibles. Y compris une grue.

15 votes

Le comportement indéfini est toujours une blague jusqu'à ce que quelqu'un est incinéré

5 votes

Au lieu de "démons nasaux", j'aime dire qu'un comportement indéfini peut appeler votre ex.

83voto

Mehrdad Points 70493

Oui, il permet tout ce qui peut arriver. La note ne fait que donner des exemples. La définition est assez claire :

Comportement non défini : comportement pour lequel la présente Norme internationale n'impose aucune exigence.


Point de confusion fréquent :

Vous devez comprendre que "aucune exigence" également signifie que la mise en œuvre est NO nécessaire pour laisser le comportement indéfini ou faire quelque chose de bizarre/non déterministe !

L'implémentation est parfaitement autorisée par la norme C++ à documenter un comportement sain et à se comporter en conséquence. 1 Donc, si votre compilateur prétend faire un enveloppement en cas de dépassement de capacité signé, la logique (la raison ?) voudrait que vous vous reposiez sur ce comportement. sur ce compilateur . N'attendez pas d'un autre compilateur qu'il se comporte de la même manière s'il ne le prétend pas.

<sup>1 </sup>Il est même permis de documenter une chose et d'en faire une autre. Ce serait stupide, et cela vous ferait probablement jeter le compilateur à la poubelle - pourquoi faire confiance à un compilateur dont la documentation vous ment - mais ce n'est pas contre le standard C++.

3 votes

Il est intéressant, cependant, de comparer les exemples normatifs qui reflétaient vraisemblablement le sens voulu de la phrase, avec les comportements des compilateurs modernes. Je n'ai pas vu la moindre preuve que les auteurs de la norme avaient l'intention que les compilateurs utilisent le comportement indéfini pour déterminer quelles entrées un programme recevrait ou ne recevrait pas.

15 votes

@supercat Les exemples et les notes ne sont pas normatifs.

6 votes

@supercat : Il était tout à fait évident que l'intention était essentiellement de "déterminer quelles entrées un programme ne recevrait pas" - c'est juste que les compilateurs n'étaient pas si avancés à l'époque. Par exemple, le but de x<<n étant UB lorsque n est égale ou supérieure à la largeur de x est que le compilateur peut simplement assumer n ne le fait pas et ne pas avoir à mettre en œuvre une logique complexe et coûteuse pour savoir ce qu'il faut faire dans ce cas. D'un point de vue conceptuel, il n'y a aucune différence entre cette optimisation et d'autres DCE plus avancées basées sur UB.

24voto

supercat Points 25534

L'un des objectifs historiques du comportement indéfini était de permettre la possibilité que certaines actions puissent avoir des effets différents sur l'environnement. potentiellement utile effets sur les différentes plateformes. Par exemple, dans les premiers jours du C, étant donné

int i=INT_MAX;
i++;
printf("%d",i);

certains compilateurs pouvaient garantir que le code imprimerait une valeur particulière (pour une machine à deux compléments, ce serait typiquement INT_MIN), tandis que d'autres garantissaient que le programme se terminerait sans atteindre le printf. Selon les exigences de l'application, l'un ou l'autre comportement peut être utile. Laisser le comportement indéfini signifiait qu'une application où une terminaison anormale du programme était une conséquence acceptable d'un débordement, mais où la production d'une sortie apparemment valide mais fausse ne l'était pas, pouvait renoncer à la vérification du débordement si elle était exécutée sur une plate-forme qui le piégerait de manière fiable, et une application où une terminaison anormale en cas de débordement n'était pas acceptable, mais où la production d'une sortie arithmétiquement incorrecte l'était, pouvait renoncer à la vérification du débordement si elle était exécutée sur une plate-forme où les débordements n'étaient pas piégés.

Récemment, cependant, certains auteurs de compilateurs semblent s'être lancés dans un concours pour voir qui peut éliminer le plus efficacement tout code dont l'existence ne serait pas imposée par la norme. Étant donné, par exemple...

#include <stdio.h>

int main(void)
{
  int ch = getchar();
  if (ch < 74)
    printf("Hey there!");
  else
    printf("%d",ch*ch*ch*ch*ch);
}

un compilateur hyper-moderne peut conclure que si ch est égal ou supérieur à 74, le calcul de ch*ch*ch*ch*ch produirait un comportement indéfini, et en tant que le programme devrait imprimer "Hey there !" inconditionnellement quel que soit le quel que soit le caractère tapé.

3 votes

Wow. Une idée de comment nous sommes passés de "potentiellement utile" à la situation actuelle, dans laquelle une grande partie de la communauté C++ semble catégoriquement opposée à toute tentative de déterminer le comportement exact de certains compilateurs lorsqu'ils rencontrent une situation permettant l'UB, avec l'explication "ça n'a pas d'importance, votre programme a l'UB" ?

1 votes

Tout est question de repères

12 votes

Non, c'est une question de portabilité. Nous vivons dans une ère d'interconnexion où les logiciels sont distribués plus vite que vous ne pouvez le penser. Nous ne sommes plus en train d'écrire des programmes pour le superordinateur poussiéreux de la cave. Du moins, la plupart d'entre nous ne le font pas. C'est en fait le résultat d'un changement de paradigme vieux de plusieurs décennies dans la programmation ; il y a maintenant des avantages pratiques tangibles à coder rigoureusement selon les normes (ce que nous aurions toujours fait dans l'idéal), et les auteurs de chaînes d'outils peuvent en tirer parti pour produire des compilateurs vraiment rapides et efficaces. Pourquoi pas ?

17voto

user5250294 Points 119

Nitpicking : Vous n'avez pas cité de norme.

Ce sont les sources utilisées pour générer les versions préliminaires de la norme C++. Ces sources ne doivent pas être considérées comme une publication de l'ISO, ni les documents générés à partir d'elles, à moins qu'elles ne soient officiellement adoptées par le groupe de travail C++ (ISO/CEI JTC1/SC22/WG21).

Interprétation : Les notes ne sont pas normatif conformément aux directives ISO/CEI, partie 2.

Les notes et les exemples intégrés dans le texte d'un document ne doivent être utilisés que pour donner des informations complémentaires destinées à faciliter la compréhension ou l'utilisation du document. Ils ne doivent pas contenir d'exigences ("doit" ; voir 3.3.1 et tableau H.1) ou toute information considérée comme indispensable à l'utilisation du document par exemple, des instructions (impératif ; voir tableau H.1), des recommandations ("devrait" ; voir 3.3.2 et tableau H.2) ou une permission ("peut" ; voir tableau H.3). Les notes peuvent être rédigées comme une déclaration de fait.

C'est moi qui souligne. Ce seul fait exclut la "liste complète des options". En revanche, le fait de donner des exemples est considéré comme une "information complémentaire destinée à faciliter la compréhension [ ] du document".

Gardez à l'esprit que le mème du "démon nasal" n'est pas à prendre au pied de la lettre, tout comme l'utilisation d'un ballon pour expliquer le fonctionnement de l'expansion de l'univers n'a rien à voir avec la réalité physique. C'est pour illustrer le fait qu'il est imprudent de discuter de ce que le "comportement indéfini" devrait faire quand il est permis de faire n'importe quoi. Oui, cela signifie qu'il n'y a pas de véritable élastique dans l'espace.

1 votes

Re : nitpick : J'ai été inspiré d'aller chercher cette citation dans le projet de norme en la voyant citée dans la norme 2003 dans une autre réponse. La formulation était très similaire, donc je ne pense pas que la formulation ait beaucoup changé depuis au moins une décennie, c'est pourquoi je me suis sentie à l'aise pour citer le projet (en plus, c'est gratuit et en ligne).

4 votes

Les versions finales de ces normes ne sont pas disponibles gratuitement, mais derrière un paywall assez élevé, et ne peuvent donc pas être liées. Cependant, les projets finaux sont identiques à la version finale dans tous les aspects techniques et linguistiques pertinents. Sans ces projets, les citations et les références à la norme sont en fait impossibles. Que préférez-vous donc : 1) la citation du projet final (et identique à cet égard) ou 2) aucune citation, ce qui revient à affirmer sans fondement ? (et comment savez-vous qu'il y a pas de élastique dans l'espace ?)

0 votes

Notez que la norme C utilise le terme "shall" d'une manière qui diffère de l'utilisation de ce terme dans presque toutes les autres normes. Dans la plupart des normes, la violation d'une contrainte rendrait une implémentation non conforme, mais ce n'est pas le cas de la norme C. Un programme qui viole une contrainte ne peut être strictement conforme, mais la norme reconnaît comme "conformes", et est expressément destinée à ne pas dévaloriser, des programmes non portables auxquels elle n'impose aucune exigence, mais dont le comportement est utilement défini par certaines implémentations.

14voto

Peter Points 4026

La définition d'un comportement non défini, dans chaque norme C et C++, est essentiellement que la norme n'impose aucune exigence sur ce qui se passe.

Oui, cela signifie que toute issue est permise. Mais il n'y a pas de résultats particuliers qui sont requis de se produire, ni de résultats qui sont requis de ne pas se produire. Peu importe que vous disposiez d'un compilateur et d'une bibliothèque qui produisent systématiquement un comportement particulier en réponse à un cas particulier de comportement non défini - un tel comportement n'est pas requis et peut changer même dans une future version de correction de bogues de votre compilateur - et le compilateur sera toujours parfaitement correct selon chaque version des normes C et C++.

Si votre système hôte dispose d'un support matériel sous la forme d'une connexion à des sondes qui sont insérées dans vos narines, il est tout à fait possible qu'une occurrence de comportement non défini provoque des effets nasaux indésirables.

6 votes

Historiquement, le fait que la norme ne définisse pas un comportement n'implique en aucun cas que les implémentations ne doivent pas le faire. En effet, un certain nombre de choses qui déclenchent un comportement indéfini le font parce qu'avant la ratification de la norme C, différentes implémentations ont fait deux (ou plus) garanties contradictoires, qui ont toutes deux été invoquées par les programmes écrits pour ces implémentations.

0 votes

@supercat : Merci pour ça ! Comme d'habitude, j'apprécie grandement votre vision historique.

0 votes

Très vrai, supercat. Il y a plusieurs raisons pour lesquelles quelque chose est indéfini. L'une d'entre elles est qu'un certain nombre de fournisseurs de compilateurs/bibliothèques - et leurs clients - ne voulaient pas perdre des fonctionnalités particulières antérieures à la norme. La seule façon d'obtenir un consensus était de rendre ces fonctionnalités non définies (ou définies par l'implémentation, non spécifiées, etc.) et de permettre une liberté d'implémentation.

8voto

Muzer Points 224

Je pensais ne pas répondre à une de vos points, puisque les autres réponses de répondre à la question générale assez bien, mais ils ont laissé ce n'est fait.

"En ignorant la situation -- Oui, le standard va jusqu'à dire que cela va avoir des "conséquences imprévisibles", mais ce n'est pas le même que le compilateur de l'insertion de code (ce qui je suppose sera une condition préalable, vous le savez, les démons)."

Une situation dans laquelle nasale démons peut très raisonnablement s'attendre à se produire avec un bon compilateur, sans le compilateur d'insérer n'IMPORTE quel code, serait la suivante:

if(!spawn_of_satan)
    printf("Random debug value: %i\n", *x); // oops, null pointer deference
    nasal_angels();
else
    nasal_demons();

Un compilateur, si elle peut prouver que *x est un déréférencement de pointeur null, a parfaitement le droit, dans le cadre de l'optimisation, de la à dire "OK, donc, je vois qu'ils ont déréférencé un pointeur null dans cette branche de la si. Par conséquent, dans le cadre de cette branche, je suis autorisé à faire quoi que ce soit. Donc, je peux donc d'optimiser:"

if(!spawn_of_satan)
    nasal_demons();
else
    nasal_demons();

"Et à partir de là, je peux optimiser:"

nasal_demons();

Vous pouvez voir comment ce genre de chose peut dans certaines circonstances s'avérer très utile pour une optimisation du compilateur, et encore provoquer des catastrophes. Je ne vois quelques exemples tout à l'arrière de cas où, en fait, il EST important pour l'optimisation d'être en mesure d'optimiser ce genre de cas. Je pourrais essayer de creuser plus tard quand j'aurai plus de temps.

EDIT: Un exemple qui vient du plus profond de mon mémoire d'un cas où il est utile pour l'optimisation de l'endroit où vous très fréquemment un pointeur NUL (peut-être en inline fonctions d'assistance), même après avoir déjà déréférencé et sans avoir changé. L'optimisation du compilateur peut voir que vous avez déréférencé et ainsi d'optimiser tous les "est NULL" vérifications, car si vous avez déréférencé et il EST nul, tout est permis, y compris tout simplement pas l'exécution de l' "est NULL" contrôles. Je crois que les mêmes arguments s'appliquent à d'autres comportements indéfinis.

0 votes

Err, désolé @supercat, j'ai en quelque sorte manqué la deuxième partie de votre réponse, qui explique aussi cela !

1 votes

...oui, je réalise que si l'utilisateur demande pour les démons nasaux dans certains cas, alors ils pourraient être convoqués dans des cas inattendus si le programme a UB. Quand je dis que certains comportements d'UB nécessiteraient l'insertion de code, je parle de comportements complètement inattendus qui ne sont pas déjà explicitement écrits dans votre code.

0 votes

Il doit y avoir un cas particulier où il est étrangement plus efficace de générer un code complètement nouveau qui tire parti de l'UB. Je vais ressortir certains des articles que j'ai lus plus tard.

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