86 votes

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

Voici un exemple pour illustrer ma question qui implique un code beaucoup plus compliqué que je ne peux pas afficher 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 non défini sur ma plateforme car a débordera sur la 3ème boucle.

Est-ce que cela rend le programme complet ont un comportement indéfini, ou seulement après que l'option le débordement se produit réellement ? Le compilateur pourrait-il potentiellement comprendre que a sera afin de pouvoir déclarer la boucle entière indéfinie et ne pas prendre la peine d'exécuter les printfs même s'ils se produisent tous avant le débordement ?

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

7 votes

Je me demande si le compilateur pourrait comprendre que a n'est pas utilisé (sauf pour le calcul lui-même) et supprimez simplement a

12 votes

Vous pourriez apprécier Mon petit optimiseur : Le comportement indéfini est magique du CppCon cette année. Il s'agit de savoir quelles optimisations les compilateurs peuvent effectuer sur la base de comportements non définis.

2 votes

108voto

TartanLlama Points 1461

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

[intro.execution]/5: 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 l'une de ces exécutions contient une opération non définie, la présente Norme internationale n'impose aucune exigence à l'iCan. n'impose aucune exigence à l'implémentation qui exécute 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).

En tant que tel, si votre programme contient un comportement non défini, alors le comportement de votre programme complet est indéfinie.

4 votes

@KeithThompson : Mais alors, la sneeze() La fonction elle-même est indéfinie sur tout ce qui est de la classe Demon (dont la variété nasale est une sous-classe), rendant l'ensemble circulaire de toute façon.

1 votes

Mais printf pourrait ne pas retourner, donc les deux premiers tours sont définis parce que jusqu'à ce qu'ils soient faits, il n'est pas clair qu'il y aura un jour UB. Voir stackoverflow.com/questions/23153445/

1 votes

C'est pourquoi un compilateur est techniquement dans son droit d'émettre "nop" pour le noyau Linux (parce que le code d'amorçage repose sur un comportement non défini) : blog.regehr.org/archives/761

31voto

Matthieu M. Points 101624

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 cimenter le tout, en gardant à l'esprit qu'aucune section n'est exhaustive :

  • le compilateur peut supposer que les portions 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 par nul autre que Chris Lattner.
  • l'éditeur de liens peut supposer qu'en présence de multiples définitions d'un symbole faible (reconnu par son nom), toutes les définitions sont identiques grâce à l'attribut Règle de la définition unique
  • le chargeur (dans le cas où vous utilisez des bibliothèques dynamiques) peut supposer la même chose, en choisissant le premier symbole qu'il trouve ; ceci est généralement (ab)utilisé pour intercepter les appels utilisant LD_PRELOAD astuces sur les Unix
  • l'exécution peut échouer (SIGSEV) si vous utilisez des pointeurs pendants.

C'est ce qui est si effrayant à propos du comportement indéfini : il est pratiquement impossible de prédire, à l'avance, le comportement exact qui 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 vous recommande de regarder cette vidéo de Michael Spencer (Développeur LLVM) : CppCon 2016 : Mon petit optimiseur : Le comportement indéfini est magique .

3 votes

C'est ce qui me préoccupe. Dans mon code réel, c'est complexe mais je pourrait avoir un cas où il débordera toujours. Et je ne me soucie pas vraiment de cela, mais je crains que le code "correct" soit également affecté par cela. Il est évident que je dois corriger ce problème, mais pour le corriger, il faut comprendre :)

8 votes

@jcoder : Il y a une échappatoire 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 le comportement indéfini ne se produit pas, le compilateur doit s'assurer que cette entrée particulière produit toujours la bonne sortie. Toutes les discussions effrayantes sur les optimisations dangereuses ne s'appliquent qu'aux éléments suivants inévitable UB. En pratique, si vous aviez utilisé argc comme le nombre de boucles, le cas argc=1 ne produit pas d'UB et le compilateur serait obligé de gérer cela.

0 votes

@jcoder : Dans ce cas, ce n'est pas du code mort. Le compilateur, cependant, pourrait être assez intelligent pour déduire que i ne peut être incrémenté plus que N fois et donc que sa valeur est bornée.

28voto

Bathsheba Points 23209

Un compilateur C ou C++ à optimisation agressive ciblant un système d'exploitation 16 bits. int sera conozca que le comportement sur l'ajout 1000000000 à un int le type est indéfini .

Les deux normes lui permettent de faire tout ce qu'il veut, ce qui pourrait incluent la suppression de l'ensemble du programme, en laissant int main(){} .

Mais qu'en est-il des plus grandes int s ? 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 bit int ou plus se rendra compte que la boucle est infinie ( i ne change pas) y así que a finira par déborder. Donc, une fois de plus, il peut optimiser la sortie à int main(){} . Ce que j'essaie de dire ici, c'est qu'au fur et à mesure que les optimisations des compilateurs deviennent de plus en plus agressives, de plus en plus de constructions à comportement indéfini 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.

3 votes

La norme l'autorise-t-elle à faire ce qu'elle veut avant même que le comportement non défini ne se manifeste ? Où cela est-il indiqué ?

4 votes

Pourquoi 16 bits ? Je suppose que l'OP cherche un débordement signé de 32 bits.

0 votes

@4386427 : j'aurais juré que la question mentionnait un int de 16 bits. Mais le fait qu'elle ne le mentionne pas rend la réponse plus intéressante.

11voto

DragonLord Points 625

Techniquement, selon la norme C++, si un programme contient un comportement non défini, le comportement du programme entier, même au moment de la compilation (avant même que le programme ne soit exécuté), est indéfinie.

En pratique, étant donné que 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 soit probable que vous obteniez des résultats corrects avant la troisième itération. Cependant, puisque 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 se planter au moment de l'exécution à n'importe quel moment de l'exécution, ou même de ne pas réussir à compiler du tout (puisque le comportement indéfini s'étend au moment de la compilation).

Les comportements non définis offrent au compilateur une plus grande marge de manœuvre pour l'optimisation, car ils éliminent 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. Vous ne devez donc pas vous fier à un comportement particulier considéré comme indéfini par la norme C++.

0 votes

Que faire si la partie UB se trouve dans un if(false) {} portée ? Est-ce que cela empoisonne tout le programme, parce que le compilateur suppose que toutes les branches contiennent des portions de logique bien définies, et fonctionne donc sur des hypothèses erronées ?

1 votes

La norme n'impose aucune exigence en matière de comportement non défini. en théorie oui, cela empoisonne l'ensemble du programme. Cependant, en pratique En effet, tout compilateur optimisateur se contentera probablement de supprimer le code mort, ce qui n'aura probablement aucun effet sur l'exécution. Vous ne devriez cependant pas vous fier à ce comportement.

0 votes

Bon à savoir, merci :)

9voto

alain Points 1226

Pour comprendre pourquoi un comportement non défini peut Voyage dans le temps", comme le dit si bien @TartanLlama. Dans ce cas, examinons la règle "as-if" :

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 internationale n'impose aucune exigence sur la structure des implémentations implémentations conformes. En particulier, elles n'ont pas besoin de copier ou d'émuler la structure de la machine abstraite. structure de la machine abstraite. Au contraire, les implémentations conformes conformes doivent émuler (uniquement) le comportement observable de la machine abstraite comme expliqué ci-dessous.

Ainsi, nous pouvons considérer 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. La sortie est le "comportement observable" mentionné dans la norme.

La norme ne définit qu'un mappage entre l'entrée et la sortie, rien d'autre. Pour ce faire, elle décrit un "exemple de boîte noire", mais indique explicitement que toute autre boîte noire présentant la même correspondance est également valable. Cela signifie que le contenu de la boîte noire n'est pas pertinent.

Dans cette optique, il serait absurde de dire qu'un comportement indéfini se produit à un moment donné. Dans le échantillon de la boîte noire, nous pourrions dire où et quand cela se produit, mais la réel La boîte noire pourrait être quelque chose de complètement différent, donc on ne peut plus dire où et quand ça se passe. En théorie, un compilateur pourrait par exemple décider d'énumérer toutes les entrées possibles et de pré-calculer les sorties résultantes. Le comportement indéfini se serait alors produit pendant la compilation.

Un comportement indéfini est l'inexistence d'une correspondance entre l'entrée et la sortie. Un programme peut avoir un comportement indéfini pour certaines entrées, mais un comportement défini pour d'autres. Dans ce cas, la correspondance entre l'entrée et la sortie est simplement incomplète ; il existe une entrée pour laquelle il n'y a pas de correspondance avec la sortie.
Le programme dans la question a un comportement indéfini pour toute entrée, donc la correspondance est vide.

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