À quel moment de la boucle le débordement d'entier devient-il un comportement indéfini?

86

Ceci est un exemple pour illustrer ma question qui implique un code beaucoup plus compliqué que je ne peux pas publier ici.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Ce programme contient un comportement indéfini sur ma plateforme car ail débordera sur la 3ème boucle.

Est-ce que cela fait que tout le programme a un comportement indéfini, ou seulement après le débordement ? Pourrait éventuellement travailler sur le compilateur qui a va déborder de sorte qu'il peut déclarer la boucle entière non défini et pas la peine de courir les printfs même si elles se produisent avant tout le trop - plein?

(Tagged C et C ++ même s'ils sont différents car je serais intéressé par des réponses pour les deux langues si elles sont différentes.)

jcoder
la source
7
aa
Je me
12
Vous aimerez peut-être My Little Optimizer: Undefined Behavior is Magic de CppCon cette année. Tout dépend des optimisations que les compilateurs peuvent effectuer en fonction d'un comportement non défini.
TartanLlama

Réponses:

108

Si vous êtes intéressé par une réponse purement théorique, la norme C ++ autorise un comportement indéfini à «voyager dans le temps»:

[intro.execution]/5: Une implémentation conforme exécutant un programme bien formé produira le même comportement observable que l'une des exécutions possibles de l'instance correspondante de la machine abstraite avec le même programme et la même entrée. Cependant, si une telle exécution contient une opération indéfinie, la présente Norme internationale n'impose aucune exigence sur l'implémentation exécutant ce programme avec cette entrée (pas même en ce qui concerne les opérations précédant la première opération non définie)

En tant que tel, si votre programme contient un comportement non défini, le comportement de l' ensemble de votre programme n'est pas défini.

TartanLlama
la source
4
@KeithThompson: Mais alors, la sneeze()fonction elle-même n'est pas définie sur tout élément de la classe Demon(dont la variété nasale est une sous-classe), ce qui rend le tout circulaire de toute façon.
Sebastian Lenartowicz
1
Mais printf peut ne pas revenir, donc les deux premiers tours sont définis car tant qu'ils ne sont pas terminés, il n'est pas clair qu'il y aura jamais UB. Voir stackoverflow.com/questions/23153445/…
usr
1
C'est pourquoi un compilateur a techniquement le droit d'émettre "nop" pour le noyau Linux (car le code d'amorçage repose sur un comportement indéfini): blog.regehr.org/archives/761
Crashworks
3
@Crashworks Et c'est pourquoi Linux est écrit et compilé en tant que C. non portable (c'est-à-dire un sur-ensemble de C qui nécessite un compilateur particulier avec des opitions particulières, telles que -fno-strict-aliasing)
user253751
3
@usr Je pense qu'il est défini si printfne retourne pas, mais s'il printfva revenir, alors le comportement non défini peut causer des problèmes avant d' printfêtre appelé. Par conséquent, voyage dans le temps. printf("Hello\n");puis la ligne suivante compile commeundoPrintf(); launchNuclearMissiles();
user253751
31

Tout d'abord, permettez-moi de corriger le titre de cette question:

Le comportement indéfini n'est pas (spécifiquement) du domaine de l'exécution.

Le comportement indéfini affecte toutes les étapes: compilation, liaison, chargement et exécution.

Quelques exemples pour consolider cela, gardez à l'esprit qu'aucune section n'est exhaustive:

  • le compilateur peut supposer que les parties de code qui contiennent un comportement indéfini ne sont jamais exécutées, et donc supposer que les chemins d'exécution qui y mèneraient sont du code mort. Voir ce que tout programmeur C devrait savoir sur le comportement indéfini de nul autre que Chris Lattner.
  • l'éditeur de liens peut supposer qu'en présence de plusieurs définitions d'un symbole faible (reconnu par son nom), toutes les définitions sont identiques grâce à la règle One Definition
  • le chargeur (dans le cas où vous utilisez des bibliothèques dynamiques) peut assumer la même chose, choisissant ainsi le premier symbole qu'il trouve; ceci est généralement (ab) utilisé pour intercepter les appels en utilisant des LD_PRELOADastuces sous Unix
  • l'exécution peut échouer (SIGSEV) si vous utilisez des pointeurs pendants

C'est ce qui fait si peur à propos du comportement indéfini: il est quasiment impossible de prédire, à l'avance, quel comportement exact se produira, et cette prédiction doit être revue à chaque mise à jour de la chaîne d'outils, du système d'exploitation sous-jacent, ...


Je recommande de regarder cette vidéo de Michael Spencer (développeur LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .

Matthieu M.
la source
3
C'est ce qui m'inquiète. Dans mon vrai code, c'est complexe mais je pourrais avoir un cas où il débordera toujours. Et je ne me soucie pas vraiment de cela, mais je crains que le code «correct» ne soit également affecté par cela. Évidemment, je dois le réparer, mais la réparation nécessite de comprendre :)
jcoder
8
@jcoder: Il y a une évasion importante ici. Le compilateur n'est pas autorisé à deviner les données d'entrée. Tant qu'il y a au moins une entrée pour laquelle un comportement indéfini ne se produit pas, le compilateur doit s'assurer que cette entrée particulière produit toujours la bonne sortie. Tous les discours effrayants sur les optimisations dangereuses ne s'appliquent qu'aux inévitables UB. En pratique, si vous aviez utilisé argcle nombre de boucles, le cas argc=1ne produit pas d'UB et le compilateur serait obligé de gérer cela.
MSalters
@jcoder: Dans ce cas, ce n'est pas du code mort. Le compilateur, cependant, pourrait être assez intelligent pour en déduire qu'il ine peut pas être incrémenté plus de Nfois et donc que sa valeur est limitée.
Matthieu M.
4
@jcoder: Si f(good);fait quelque chose X et f(bad);invoque un comportement indéfini, alors un programme qui appelle simplement f(good);est garanti de faire X, mais il f(good); f(bad);n'est pas garanti de faire X.
4
@Hurkyl plus intéressant, si votre code est if(foo) f(good); else f(bad);, un compilateur intelligent jettera la comparaison et produira et un inconditionnel foo(good).
John Dvorak
28

Une optimisation agressive compilateur C ou de C ciblant un 16 bits intva savoir que le comportement sur l' ajout d' 1000000000un inttype est indéfini .

Il est permis par l'une ou l'autre des normes de faire tout ce qu'il veut, ce qui pourrait inclure la suppression de tout le programme, en partant int main(){}.

Mais qu'en est-il des plus grands int? Je ne connais pas encore de compilateur qui fasse cela (et je ne suis en aucun cas un expert en conception de compilateurs C et C ++), mais j'imagine que parfois un compilateur ciblant un 32 bits intou plus comprendra que la boucle est infini ( ine change pas) et donc afinira par déborder. Donc, encore une fois, il peut optimiser la sortie vers int main(){}. Le point que j'essaie de faire ici est que, à mesure que les optimisations du compilateur deviennent progressivement plus agressives, de plus en plus de constructions de comportement indéfinies se manifestent de manière inattendue.

Le fait que votre boucle soit infinie n'est pas en soi indéfini puisque vous écrivez sur la sortie standard dans le corps de la boucle.

Bathsheba
la source
3
La norme est-elle autorisée à faire tout ce qu'elle veut avant même que le comportement non défini ne se manifeste? Où est-ce indiqué?
jimifiki
4
pourquoi 16 bits? Je suppose qu'OP recherche un débordement signé 32 bits.
4386427
8
@jimifiki Dans le standard. C ++ 14 (N4140) 1.3.24 "Comportement défini = comportement pour lequel la présente Norme internationale n'impose aucune exigence." Plus une longue note qui élabore. Mais le fait est que ce n'est pas le comportement d'une «instruction» qui n'est pas définie, c'est le comportement du programme. Cela signifie que tant que l'UB est déclenché par une règle de la norme (ou par l'absence de règle), la norme cesse de s'appliquer pour le programme dans son ensemble. Ainsi, n'importe quelle partie du programme peut se comporter comme elle le souhaite.
Angew n'est plus fier de SO
5
La première déclaration est fausse. Si intest 16 bits, l'ajout aura lieu dans long(parce que l'opérande littéral a le type long) où il est bien défini, puis sera converti par une conversion définie par l'implémentation en int.
R .. GitHub STOP AIDER ICE
2
@usr le comportement de printfest défini par la norme pour toujours retourner
MM
11

Techniquement, sous la norme C ++, si un programme contient un comportement non défini, le comportement de l'ensemble du programme, même au moment de la compilation (avant même que le programme ne soit exécuté), n'est pas défini.

En pratique, comme le compilateur peut supposer (dans le cadre d'une optimisation) que le débordement ne se produira pas, au moins le comportement du programme à la troisième itération de la boucle (en supposant une machine 32 bits) sera indéfini, bien qu'il Il est probable que vous obtiendrez des résultats corrects avant la troisième itération. Cependant, comme le comportement de l'ensemble du programme est techniquement indéfini, rien n'empêche le programme de générer une sortie complètement incorrecte (y compris aucune sortie), de planter à l'exécution à tout moment pendant l'exécution, ou même de ne pas compiler complètement (car le comportement non défini s'étend à temps de compilation).

Un comportement non défini offre au compilateur plus de marge d'optimisation car il élimine certaines hypothèses sur ce que le code doit faire. Ce faisant, les programmes qui reposent sur des hypothèses impliquant un comportement non défini ne sont pas garantis de fonctionner comme prévu. En tant que tel, vous ne devez pas vous fier à un comportement particulier considéré comme non défini par la norme C ++.

bwDraco
la source
Et si la partie UB est dans un if(false) {}périmètre? Cela empoisonne-t-il tout le programme, du fait que le compilateur suppose que toutes les branches contiennent des parties logiques bien définies et donc fonctionnant sur de fausses hypothèses?
mlvljr
1
La norme n'impose aucune exigence sur un comportement indéfini, donc en théorie , oui, elle empoisonne tout le programme. Cependant, en pratique , tout compilateur d'optimisation supprimera probablement simplement le code mort, donc cela n'aurait probablement aucun effet sur l'exécution. Cependant, vous ne devriez toujours pas vous fier à ce comportement.
bwDraco
Bon à savoir, merci :)
mlvljr
9

Pour comprendre pourquoi un comportement non défini peut `` voyager dans le temps '' comme le dit correctement @TartanLlama , jetons un coup d'œil à la règle du `` comme si '':

1.9 Exécution du programme

1 Les descriptions sémantiques de la présente Norme internationale définissent une machine abstraite non déterministe paramétrée. La présente Norme internationale n'impose aucune exigence sur la structure des implémentations conformes. En particulier, ils n'ont pas besoin de copier ou d'émuler la structure de la machine abstraite. Au contraire, des implémentations conformes sont nécessaires pour émuler (uniquement) le comportement observable de la machine abstraite comme expliqué ci-dessous.

Avec cela, nous pourrions voir le programme comme une «boîte noire» avec une entrée et une sortie. L'entrée peut être une entrée utilisateur, des fichiers et bien d'autres choses. Le résultat est le «comportement observable» mentionné dans la norme.

La norme définit uniquement un mappage entre l'entrée et la sortie, rien d'autre. Il le fait en décrivant un «exemple de boîte noire», mais dit explicitement que toute autre boîte noire avec le même mappage est également valide. Cela signifie que le contenu de la boîte noire n'est pas pertinent.

Dans cet esprit, il ne serait pas logique de dire qu'un comportement indéfini se produit à un certain moment. Dans l' échantillon mise en œuvre de la boîte noire, on pourrait dire où et quand il arrive, mais le réel boîte noire pourrait être quelque chose de complètement différent, nous ne pouvons donc plus dire où et quand cela se produit. Théoriquement, un compilateur pourrait par exemple décider d'énumérer toutes les entrées possibles, et pré-calculer les sorties résultantes. Ensuite, le comportement non défini se serait produit lors de la compilation.

Un comportement indéfini est l'inexistence d'un mappage entre l'entrée et la sortie. Un programme peut avoir un comportement non défini pour certaines entrées, mais un comportement défini pour d'autres. Ensuite, le mappage entre l'entrée et la sortie est tout simplement incomplet; il y a une entrée pour laquelle aucun mappage à la sortie n'existe.
Le programme de la question a un comportement indéfini pour toute entrée, le mappage est donc vide.

Alain
la source
6

En supposant qu'il s'agisse de int32 bits, un comportement indéfini se produit à la troisième itération. Ainsi, si, par exemple, la boucle n'était accessible que conditionnellement, ou pouvait être interrompue conditionnellement avant la troisième itération, il n'y aurait pas de comportement indéfini à moins que la troisième itération ne soit réellement atteinte. Cependant, en cas de comportement indéfini, toute la sortie du programme est indéfinie, y compris la sortie qui est "dans le passé" par rapport à l'invocation d'un comportement non défini. Par exemple, dans votre cas, cela signifie qu'il n'y a aucune garantie de voir 3 messages "Hello" dans la sortie.

R .. GitHub STOP AIDING ICE
la source
6

La réponse de TartanLlama est correcte. Le comportement non défini peut se produire à tout moment, même pendant la compilation. Cela peut sembler absurde, mais c'est une fonctionnalité clé pour permettre aux compilateurs de faire ce qu'ils doivent faire. Ce n'est pas toujours facile d'être un compilateur. Vous devez faire exactement ce que dit la spécification, à chaque fois. Cependant, il peut parfois être monstrueusement difficile de prouver qu'un comportement particulier se produit. Si vous vous souvenez du problème d'arrêt, il est plutôt trivial de développer un logiciel pour lequel vous ne pouvez pas prouver s'il termine ou entre dans une boucle infinie lorsqu'il est alimenté par une entrée particulière.

Nous pourrions rendre les compilateurs pessimistes et compiler constamment de peur que la prochaine instruction ne soit l'un de ces problèmes comme des problèmes, mais ce n'est pas raisonnable. Au lieu de cela, nous donnons un laissez-passer au compilateur: sur ces sujets de «comportement indéfini», ils sont libérés de toute responsabilité. Un comportement indéfini comprend tous les comportements qui sont si subtilement néfastes que nous avons du mal à les séparer des problèmes d'arrêt vraiment méchants et néfastes, etc.

Il y a un exemple que j'aime publier, même si j'avoue avoir perdu la source, je dois donc paraphraser. C'était à partir d'une version particulière de MySQL. Dans MySQL, ils avaient un tampon circulaire qui était rempli de données fournies par l'utilisateur. Bien sûr, ils voulaient s'assurer que les données ne débordaient pas de la mémoire tampon, ils avaient donc une vérification:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

Cela semble assez sain d'esprit. Cependant, que se passe-t-il si numberOfNewChars est vraiment gros et déborde? Ensuite, il s'enroule et devient un pointeur plus petit queendOfBufferPtr , de sorte que la logique de débordement ne sera jamais appelée. Alors ils ont ajouté un deuxième chèque, avant celui-là:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Il semble que vous vous êtes occupé de l'erreur de dépassement de la mémoire tampon, non? Cependant, un bogue a été soumis indiquant que ce tampon débordait sur une version particulière de Debian! Une enquête minutieuse a montré que cette version de Debian était la première à utiliser une version particulièrement avant-gardiste de gcc. Sur cette version de gcc, le compilateur a reconnu que currentPtr + numberOfNewChars peut ne jamais être un pointeur plus petit que currentPtr car le débordement des pointeurs est un comportement indéfini! C'était suffisant pour que gcc optimise l'intégralité de la vérification, et tout à coup, vous n'étiez pas protégé contre les débordements de tampon, même si vous avez écrit le code pour le vérifier!

C'était un comportement de spécification. Tout était légal (bien que d'après ce que j'ai entendu, gcc a annulé ce changement dans la prochaine version). Ce n'est pas ce que je considérerais comme un comportement intuitif, mais si vous étirez un peu votre imagination, il est facile de voir comment une légère variante de cette situation pourrait devenir un problème d'arrêt pour le compilateur. Pour cette raison, les rédacteurs de spécifications l'ont fait "Undefined Behavior" et ont déclaré que le compilateur pouvait faire absolument tout ce qu'il voulait.

Cort Ammon
la source
Je ne considère pas particulièrement les compilateurs étonnants qui se comportent parfois comme si l'arithmétique signée était exécutée sur des types dont la plage s'étend au-delà de "int", d'autant plus que même lors de la génération de code simple sur x86, il y a des moments où cela est plus efficace que de tronquer intermédiaire résultats. Ce qui est plus étonnant, c'est lorsqu'un débordement affecte d' autres calculs, ce qui peut se produire dans gcc même si le code stocke le produit de deux valeurs uint16_t dans un uint32_t - une opération qui ne devrait avoir aucune raison plausible d'agir de manière surprenante dans une construction non assainissante.
supercat
Bien sûr, la vérification correcte serait if(numberOfNewChars > endOfBufferPtr - currentPtr), à condition que numberOfNewChars ne puisse jamais être négatif et que currentPtr pointe toujours vers quelque part dans la mémoire tampon, vous n'avez même pas besoin de la vérification ridicule de "wraparound". (Je ne pense pas que le code que vous avez fourni ait le moindre espoir de fonctionner dans un tampon circulaire - vous avez omis tout ce qui est nécessaire pour cela dans la paraphrase, alors
j'ignore
@ Random832 J'ai laissé de côté une tonne. J'ai essayé de citer le contexte plus large, mais depuis que j'ai perdu ma source, j'ai trouvé que la paraphrase du contexte m'avait causé plus de problèmes, alors je l'ai laissé de côté. J'ai vraiment besoin de trouver ce foutu rapport de bogue afin de pouvoir le citer correctement. C'est vraiment un exemple puissant de la façon dont vous pouvez penser que vous avez écrit du code dans un sens et le faire compiler complètement différemment.
Cort Ammon
C'est mon plus gros problème avec un comportement indéfini. Il est parfois impossible d'écrire du code correct, et lorsque le compilateur le détecte, par défaut, il ne vous dit pas qu'il a déclenché un comportement indéfini. Dans ce cas, l'utilisateur veut simplement faire de l'arithmétique - pointeur ou non - et tout son travail acharné pour écrire du code sécurisé a été annulé. Il devrait au moins y avoir un moyen d'annoter une section de code à dire - pas d'optimisations sophistiquées ici. C / C ++ est utilisé dans trop de domaines critiques pour permettre à cette situation dangereuse de continuer en faveur de l'optimisation
John McGrath
4

Au-delà des réponses théoriques, une observation pratique serait que pendant longtemps les compilateurs ont appliqué diverses transformations sur des boucles pour réduire la quantité de travail effectué en leur sein. Par exemple, étant donné:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

un compilateur pourrait transformer cela en:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

Économisant ainsi une multiplication à chaque itération de boucle. Une forme supplémentaire d'optimisation, que les compilateurs ont adaptée avec divers degrés d'agressivité, transformerait cela en:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

Même sur les machines avec une boucle silencieuse en cas de débordement, cela pourrait mal fonctionner s'il y avait un nombre inférieur à n qui, multiplié par l'échelle, donnerait 0. Cela pourrait également se transformer en une boucle sans fin si l'échelle était lue à partir de la mémoire plus d'une fois et quelque chose. a changé sa valeur de manière inattendue (dans tous les cas où "scale" pourrait changer à mi-boucle sans appeler UB, un compilateur ne serait pas autorisé à effectuer l'optimisation).

Alors que la plupart de ces optimisations n'auraient aucun problème dans les cas où deux types courts non signés sont multipliés pour donner une valeur comprise entre INT_MAX + 1 et UINT_MAX, gcc a certains cas où une telle multiplication dans une boucle peut provoquer une sortie anticipée de la boucle . Je n'ai pas remarqué de tels comportements provenant d'instructions de comparaison dans le code généré, mais c'est observable dans les cas où le compilateur utilise le débordement pour déduire qu'une boucle peut s'exécuter au plus 4 fois ou moins; il ne génère pas par défaut d'avertissements dans les cas où certaines entrées provoqueraient UB et d'autres pas, même si ses inférences font ignorer la limite supérieure de la boucle.

supercat
la source
4

Un comportement non défini est, par définition, une zone grise. Vous ne pouvez tout simplement pas prédire ce qu'il fera ou ne fera pas - c'est ce que signifie "comportement non défini" .

Depuis des temps immémoriaux, les programmeurs ont toujours essayé de récupérer les restes de définition d'une situation indéfinie. Ils ont un code qu'ils veulent vraiment utiliser, mais qui se révèle être non défini, de sorte qu'ils tentent d'argumenter: « Je sais que ça non défini, mais sûrement il sera, au pire, faire ceci ou cela, il ne le fera jamais que . " Et parfois, ces arguments sont plus ou moins justes - mais souvent, ils sont faux. Et à mesure que les compilateurs deviennent plus intelligents et plus intelligents (ou, diront certains, de plus en plus sournois), les limites de la question ne cessent de changer.

Donc, vraiment, si vous voulez écrire du code qui fonctionnera à coup sûr, et qui continuera à fonctionner pendant longtemps, il n'y a qu'un seul choix: éviter à tout prix le comportement indéfini. En vérité, si vous vous y mêlez, il reviendra vous hanter.

Sommet de Steve
la source
et pourtant, voici le truc ... les compilateurs peuvent utiliser un comportement indéfini pour optimiser mais ils ne vous le disent généralement pas. Donc, si nous avons cet outil génial que vous devez éviter à tout prix de faire X, pourquoi le compilateur ne peut-il pas vous avertir pour que vous puissiez le réparer?
Jason S
1

Une chose que votre exemple ne prend pas en compte est l'optimisation. aest défini dans la boucle mais jamais utilisé, et un optimiseur pourrait résoudre ce problème. En tant que tel, il est légitime que l'optimiseur rejettea complètement, et dans ce cas, tout comportement non défini disparaît comme la victime d'un boojum.

Cependant, bien sûr, cela lui-même n'est pas défini, car l'optimisation n'est pas définie. :)

Graham
la source
1
Il n'y a aucune raison d'envisager une optimisation pour déterminer si le comportement n'est pas défini.
Keith Thompson
2
Le fait que le programme se comporte comme on pourrait le supposer ne signifie pas que le comportement non défini "disparaît". Le comportement n'est toujours pas défini et vous comptez simplement sur la chance. Le fait même que le comportement du programme puisse changer en fonction des options du compilateur est un indicateur fort que le comportement n'est pas défini.
Jordan Melo
@JordanMelo Étant donné que de nombreuses réponses précédentes portaient sur l'optimisation (et l'OP a spécifiquement posé des questions à ce sujet), j'ai mentionné une fonctionnalité d'optimisation qu'aucune réponse précédente n'avait couverte. J'ai également souligné que même si l'optimisation pouvait le supprimer, le recours à l'optimisation pour fonctionner d'une manière particulière est encore une fois indéfini. Je ne le recommande certainement pas! :)
Graham
@KeithThompson Bien sûr, mais l'OP a spécifiquement posé des questions sur l'optimisation et son effet sur le comportement indéfini qu'il verrait sur sa plate-forme. Ce comportement spécifique pourrait disparaître, en fonction de l'optimisation. Cependant, comme je l'ai dit dans ma réponse, l'indéfini ne le serait pas.
Graham
0

Puisque cette question est à double balisage C et C ++, je vais essayer de répondre aux deux. C et C ++ adoptent ici des approches différentes.

En C, l'implémentation doit être capable de prouver que le comportement non défini sera invoqué afin de traiter l'ensemble du programme comme s'il avait un comportement non défini. Dans l'exemple des OP, il semblerait trivial pour le compilateur de prouver cela et c'est donc comme si tout le programme n'était pas défini.

Nous pouvons le voir à partir du rapport de défaut 109 qui, à son cœur, demande:

Si toutefois la norme C reconnaît l'existence distincte de «valeurs indéfinies» (dont la simple création n'implique pas entièrement un «comportement indéfini»), alors une personne effectuant des tests de compilateur pourrait écrire un scénario de test tel que le suivant, et il / elle pourrait également s'attendre (ou éventuellement exiger) qu'une implémentation conforme devrait, à tout le moins, compiler ce code (et éventuellement lui permettre de s'exécuter) sans «échec».

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Donc la question finale est la suivante: le code ci-dessus doit-il être "traduit avec succès" (quoi que cela signifie)? (Voir la note de bas de page jointe au paragraphe 5.1.1.3.)

et la réponse a été:

La norme C utilise le terme «valeur indéterminée» et non «valeur indéfinie». L'utilisation d'un objet à valeur indéterminée entraîne un comportement indéfini. La note de bas de page du paragraphe 5.1.1.3 indique qu'une implémentation est libre de produire n'importe quel nombre de diagnostics tant qu'un programme valide est toujours correctement traduit. Si une expression dont l'évaulation entraînerait un comportement indéfini apparaît dans un contexte où une expression constante est requise, le programme contenant n'est pas strictement conforme. De plus, si chaque exécution possible d'un programme donné aboutit à un comportement indéfini, le programme donné n'est pas strictement conforme. Une implémentation conforme ne doit pas manquer de traduire un programme strictement conforme simplement parce qu'une exécution possible de ce programme entraînerait un comportement indéfini. Comme foo pourrait ne jamais être appelé, l'exemple donné doit être traduit avec succès par une implémentation conforme.

En C ++, l'approche semble plus souple et suggérerait qu'un programme a un comportement indéfini, que l'implémentation puisse le prouver statiquement ou non.

Nous avons [intro.abstrac] p5 qui dit:

Une implémentation conforme exécutant un programme bien formé doit produire le même comportement observable que l'une des exécutions possibles de l'instance correspondante de la machine abstraite avec le même programme et la même entrée. Cependant, si une telle exécution contient une opération non définie, ce document n'impose aucune exigence sur l'implémentation exécutant ce programme avec cette entrée (même pas en ce qui concerne les opérations précédant la première opération non définie).

Shafik Yaghmour
la source
Le fait que l'exécution d'une fonction invoquerait UB ne peut affecter la façon dont un programme se comporte lorsqu'on lui donne une entrée particulière que si au moins une exécution possible du programme avec cette entrée invoquerait UB. Le fait que l'invocation d'une fonction invoquerait UB n'empêche pas un programme d'avoir un comportement défini lorsqu'il reçoit une entrée qui ne permettrait pas à la fonction d'être appelée.
supercat du
@supercat Je crois que c'est ce que ma réponse nous dit au moins pour C.
Shafik Yaghmour
Je pense que la même chose s'applique pour le texte cité concernant C ++, puisque la phrase "Toute exécution de ce type" se réfère à la manière dont le programme pourrait s'exécuter avec une entrée donnée. Si une entrée particulière ne peut pas entraîner l'exécution d'une fonction, je ne vois rien dans le texte cité pour dire que quoi que ce soit dans une telle fonction entraînerait UB.
supercat du
-2

La réponse principale est une idée fausse (mais courante):

Le comportement non défini est une propriété d' exécution *. Il NE PEUT PAS "voyager dans le temps"!

Certaines opérations sont définies (par la norme) comme ayant des effets secondaires et ne peuvent pas être optimisées. Les opérations qui effectuent des E / S ou qui accèdent aux volatilevariables appartiennent à cette catégorie.

Cependant , il y a une mise en garde: UB peut être n'importe quel comportement, y compris un comportement qui annule les opérations précédentes. Cela peut avoir des conséquences similaires, dans certains cas, à l'optimisation du code antérieur.

En fait, cela est cohérent avec la citation de la première réponse (c'est moi qui souligne):

Une implémentation conforme exécutant un programme bien formé produira le même comportement observable que l'une des exécutions possibles de l'instance correspondante de la machine abstraite avec le même programme et la même entrée.
Cependant, si une telle exécution contient une opération indéfinie, la présente Norme internationale n'impose aucune exigence sur l'implémentation exécutant ce programme avec cette entrée (pas même en ce qui concerne les opérations précédant la première opération non définie).

Oui, cette citation ne dit « pas même en ce qui concerne les opérations précédant la première opération non définie » , mais avis que cela est spécifiquement sur le code qui est exécuté , non seulement compilé.
Après tout, un comportement indéfini qui n'est pas réellement atteint ne fait rien, et pour que la ligne contenant UB soit réellement atteinte, le code qui le précède doit s'exécuter en premier!

Donc oui, une fois UB exécuté , les effets des opérations précédentes deviennent indéfinis. Mais jusqu'à ce que cela se produise, l'exécution du programme est bien définie.

Notez, cependant, que toutes les exécutions du programme qui aboutissent à ce phénomène peuvent être optimisées pour des programmes équivalents , y compris ceux qui effectuent des opérations précédentes mais annulent ensuite leurs effets. Par conséquent, le code précédent peut être optimisé chaque fois que cela équivaudrait à ce que leurs effets soient annulés ; sinon, il ne peut pas. Voir ci-dessous pour un exemple.

* Remarque: ce n'est pas incompatible avec UB qui se produit au moment de la compilation . Si le compilateur peut effectivement prouver que le code UB sera toujours exécuté pour toutes les entrées, alors UB peut s'étendre jusqu'au moment de la compilation. Cependant, cela nécessite de savoir que tout le code précédent revient finalement , ce qui est une exigence forte. Encore une fois, voir ci-dessous pour un exemple / une explication.


Pour rendre cela concret, notez que le code suivant doit s'imprimer fooet attendre votre entrée quel que soit le comportement non défini qui le suit:

printf("foo");
getchar();
*(char*)1 = 1;

Cependant, notez également qu'il n'y a aucune garantie qui foorestera à l'écran après que l'UB se produise, ou que le caractère que vous avez tapé ne sera plus dans la mémoire tampon d'entrée; ces deux opérations peuvent être «annulées», ce qui a un effet similaire au «voyage dans le temps» UB.

Si la getchar()ligne n'était pas là, il serait légal que les lignes soient optimisées si et seulement si cela ne pouvait pas être distingué de la sortie foopuis de la «non-exécution».

Que les deux soient indiscernables ou non dépendrait entièrement de l'implémentation (c'est-à-dire de votre compilateur et de votre bibliothèque standard). Par exemple, pouvez-vous printf bloquer votre thread ici en attendant qu'un autre programme lise la sortie? Ou reviendra-t-il immédiatement?

  • S'il peut bloquer ici, alors un autre programme peut refuser de lire sa sortie complète, et il peut ne jamais revenir, et par conséquent UB peut ne jamais se produire réellement.

  • S'il peut revenir immédiatement ici, alors nous savons qu'il doit revenir, et par conséquent, l'optimiser est totalement impossible à distinguer de son exécution et de son annulation.

Bien sûr, puisque le compilateur sait quel comportement est autorisé pour sa version particulière de printf, il peut optimiser en conséquence, et par conséquent printfpeut être optimisé dans certains cas et pas dans d'autres. Mais, encore une fois, la justification est que cela ne se distingue pas des opérations précédentes annulées par l'UB, non pas que le code précédent est "empoisonné" à cause de l'UB.

user541686
la source
1
Vous avez totalement mal interprété la norme. Il indique que le comportement lors de l'exécution du programme n'est pas défini. Période. Cette réponse est 100% fausse. La norme est très claire - exécuter un programme avec une entrée qui produit UB à tout moment dans le flux naïf d'exécution n'est pas défini.
David Schwartz
@DavidSchwartz: Si vous suivez votre interprétation jusqu'à ses conclusions logiques, vous devriez réaliser que cela n'a pas de sens logique. L'entrée n'est pas quelque chose qui est entièrement déterminé lorsque le programme démarre. L'entrée du programme (même sa simple présence ) sur une ligne donnée peut dépendre de tous les effets secondaires du programme jusqu'à cette ligne. Par conséquent, le programme ne peut éviter de produire les effets secondaires qui précèdent la ligne UB, car cela nécessite une interaction avec son environnement et affecte donc si la ligne UB sera atteinte ou non en premier lieu.
user541686
3
Cela n'a pas d'importance. Vraiment. Encore une fois, vous manquez simplement d'imagination. Par exemple, si le compilateur peut dire qu'aucun code conforme ne peut faire la différence, il pourrait déplacer le code qui est UB de telle sorte que la partie qui est UB s'exécute avant les sorties que vous vous attendez naïvement à "précéder".
David Schwartz
2
@Mehrdad: Peut-être qu'un meilleur moyen de dire les choses serait de dire qu'UB ne peut pas remonter dans le temps au-delà du dernier point où quelque chose aurait pu se produire dans le monde réel qui aurait défini le comportement. Si une implémentation pouvait déterminer en examinant les tampons d'entrée qu'il n'y avait aucun moyen pour l'un des 1000 prochains appels à getchar () de bloquer, et elle pourrait également déterminer que UB se produirait après le 1000ème appel, il ne serait pas nécessaire d'effectuer l'un des les appels. Si, cependant, une implémentation devait spécifier que l'exécution ne passera pas un getchar () jusqu'à ce que toutes les sorties précédentes aient ...
supercat
2
... été livré à un terminal à 300 bauds, et que tout contrôle-C qui se produit avant cela entraînera getchar () à élever un signal même s'il y avait d'autres caractères dans le tampon le précédant, alors une telle implémentation ne pourrait pas déplacez n'importe quel UB au-delà de la dernière sortie précédant un getchar (). Ce qui est difficile, c'est de savoir dans quel cas un compilateur devrait passer par le programmeur toutes les garanties comportementales qu'une implémentation de bibliothèque pourrait offrir au-delà de celles prescrites par le Standard.
supercat