Les branches avec un comportement non défini peuvent-elles être considérées comme inaccessibles et optimisées en tant que code mort?

88

Considérez la déclaration suivante:

*((char*)NULL) = 0; //undefined behavior

Il invoque clairement un comportement indéfini. L'existence d'une telle instruction dans un programme donné signifie-t-elle que l'ensemble du programme est indéfini ou que le comportement ne devient indéfini qu'une fois que le flux de contrôle atteint cette instruction?

Le programme suivant serait-il bien défini au cas où l'utilisateur n'entrerait jamais le numéro 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Ou s'agit-il d'un comportement totalement indéfini, peu importe ce que l'utilisateur entre?

De plus, le compilateur peut-il supposer qu'un comportement non défini ne sera jamais exécuté lors de l'exécution? Cela permettrait de raisonner à rebours dans le temps:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Ici, le compilateur pourrait penser qu'au cas où num == 3nous invoquerions toujours un comportement non défini. Par conséquent, ce cas doit être impossible et le numéro n'a pas besoin d'être imprimé. La ifdéclaration entière pourrait être optimisée. Ce type de raisonnement à rebours est-il autorisé selon la norme?

usr
la source
19
Parfois, je me demande si les utilisateurs avec beaucoup de représentants obtiennent plus de votes positifs sur les questions parce que "oh ils ont beaucoup de représentants, cela doit être une bonne question" ... mais dans ce cas, j'ai lu la question et j'ai pensé "wow, c'est génial "avant même de regarder le demandeur.
turbulencetoo
4
Je pense que le moment le comportement indéfini émerge, est indéfini.
eerorika
6
Le standard C ++ dit explicitement qu'un chemin d'exécution avec un comportement indéfini à tout moment est complètement indéfini. Je l'interpréterais même comme disant que tout programme avec un comportement indéfini sur le chemin est complètement indéfini (cela inclut des résultats raisonnables sur d'autres parties, mais que ce n'est pas garanti). Les compilateurs sont libres d'utiliser le comportement non défini pour modifier votre programme. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html contient quelques bons exemples.
Jens
4
@Jens: Cela ne signifie vraiment que le chemin d'exécution. Sinon, vous avez des ennuis const int i = 0; if (i) 5/i;.
MSalters
1
Le compilateur en général ne peut pas prouver qu'il PrintToConsolen'appelle pas std::exit, il doit donc effectuer l'appel.
MSalters

Réponses:

65

L'existence d'une telle instruction dans un programme donné signifie-t-elle que l'ensemble du programme n'est pas défini ou que le comportement ne devient indéfini qu'une fois que le flux de contrôle atteint cette instruction?

Ni. La première condition est trop forte et la seconde est trop faible.

Les accès aux objets sont parfois séquencés, mais la norme décrit le comportement du programme en dehors du temps. Danvil a déjà cité:

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)

Cela peut être interprété:

Si l'exécution du programme produit un comportement indéfini, alors tout le programme a un comportement indéfini.

Ainsi, une instruction inaccessible avec UB ne donne pas au programme UB. Une instruction accessible qui (à cause des valeurs des entrées) n'est jamais atteinte, ne donne pas au programme UB. C'est pourquoi votre première condition est trop forte.

Maintenant, le compilateur ne peut pas en général dire ce que contient UB. Donc, pour permettre à l'optimiseur de réorganiser les instructions avec UB potentiel qui serait réordonnable si leur comportement était défini, il est nécessaire de permettre à UB de "remonter dans le temps" et de se tromper avant le point de séquence précédent (ou en C ++ 11, pour que l'UB affecte les choses qui sont séquencées avant la chose UB). Par conséquent, votre deuxième condition est trop faible.

Un exemple majeur de ceci est lorsque l'optimiseur s'appuie sur un aliasing strict. L'intérêt des règles strictes d'aliasing est de permettre au compilateur de réorganiser les opérations qui ne pourraient pas être réordonnées valablement s'il était possible que les pointeurs en question alias la même mémoire. Ainsi, si vous utilisez des pointeurs d'aliasing illégal et que UB se produit, cela peut facilement affecter une instruction "avant" l'instruction UB. En ce qui concerne la machine abstraite, l'instruction UB n'a pas encore été exécutée. En ce qui concerne le code objet réel, il a été partiellement ou entièrement exécuté. Mais la norme n'essaie pas d'entrer dans les détails sur ce que signifie pour l'optimiseur de réorganiser les instructions, ou quelles en sont les implications pour UB. Cela donne simplement à la licence d'implémentation une erreur dès qu'il le souhaite.

Vous pouvez penser à cela comme "UB a une machine à remonter le temps".

Plus précisément pour répondre à vos exemples:

  • Le comportement n'est indéfini que si 3 est lu.
  • Les compilateurs peuvent éliminer le code comme étant mort et le font si un bloc de base contient une opération qui ne sera certainement pas définie. Ils sont autorisés (et je suppose que oui) dans les cas qui ne sont pas un bloc de base mais où toutes les branches mènent à UB. Cet exemple n'est pas un candidat à moins PrintToConsole(3)d'être connu pour être sûr de revenir. Cela pourrait lever une exception ou autre.

Un exemple similaire à votre deuxième est l'option gcc -fdelete-null-pointer-checks, qui peut prendre un code comme celui-ci (je n'ai pas vérifié cet exemple spécifique, considérez-le comme illustratif de l'idée générale):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

et changez-le en:

*p = 3;
std::cout << "3\n";

Pourquoi? Parce que si pest nul, le code a de toute façon UB, de sorte que le compilateur peut supposer qu'il n'est pas nul et optimiser en conséquence. Le noyau Linux a trébuché là-dessus ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) essentiellement parce qu'il fonctionne dans un mode où le déréférencement d'un pointeur nul n'est pas censé be UB, cela devrait entraîner une exception matérielle définie que le noyau peut gérer. Lorsque l'optimisation est activée, gcc nécessite l'utilisation de-fno-delete-null-pointer-checks afin de fournir cette garantie hors norme.

PS La réponse pratique à la question "Quand un comportement indéfini frappe-t-il?" est "10 minutes avant votre départ pour la journée".

Steve Jessop
la source
4
En fait, il y avait pas mal de problèmes de sécurité à cause de cela dans le passé. En particulier, tout contrôle de débordement après le fait risque d'être optimisé pour cette raison. Par exemple void can_add(int x) { if (x + 100 < x) complain(); }peut être optimisé disparaître entièrement, parce que si x+100 n » a rien de trop - plein se produit, et si le x+100 fait de débordement, qui est UB selon la norme, donc rien ne peut arriver.
fgp
3
@fgp: d'accord, c'est une optimisation dont les gens se plaignent amèrement s'ils trébuchent dessus, car on commence à avoir l'impression que le compilateur rompt délibérément votre code pour vous punir. "Pourquoi l'aurais-je écrit de cette façon si je voulais que tu le supprimes!" ;-) Mais je pense que parfois, il est utile à l'optimiseur lors de la manipulation d'expressions arithmétiques plus grandes, de supposer qu'il n'y a pas de débordement et d'éviter tout ce qui serait coûteux qui ne serait nécessaire que dans ces cas.
Steve Jessop
2
Serait-il correct de dire que le programme n'est pas indéfini si l'utilisateur n'entre jamais 3, mais s'il entre 3 pendant une exécution, l'exécution entière devient indéfinie? Dès qu'il est certain à 100% que le programme invoquera un comportement non défini (et pas plus tôt que cela), le comportement devient n'importe quoi. Ces déclarations sont-elles exactes à 100%?
usr
3
@usr: Je pense que c'est exact, oui. Avec votre exemple particulier (et en faisant certaines hypothèses sur l'inévitabilité des données en cours de traitement), je pense qu'une implémentation pourrait en principe anticiper dans STDIN tamponné 3si elle le souhaitait, et ranger à la maison pour la journée dès qu'elle en a vu un entrant.
Steve Jessop
3
Un +1 supplémentaire (si je pouvais) pour votre PS
Fred Larson
10

La norme indique à 1,9 / 4

[Remarque: La présente Norme internationale n'impose aucune exigence sur le comportement des programmes qui contiennent un comportement indéfini. - note de fin]

Le point intéressant est probablement ce que signifie «contenir». Un peu plus tard, à 1,9 / 5, il déclare:

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)

Ici, il mentionne spécifiquement "l'exécution ... avec cette entrée". J'interpréterais cela comme un comportement indéfini dans une branche possible qui n'est pas exécutée actuellement n'influence pas la branche d'exécution actuelle.

Les hypothèses basées sur un comportement indéfini lors de la génération de code constituent un autre problème. Voir la réponse de Steve Jessop pour plus de détails à ce sujet.

Danvil
la source
1
Si elle est prise à la lettre, c'est la peine de mort pour tous les programmes réels existants.
usr
6
Je ne pense pas que la question soit de savoir si UB peut apparaître avant que le code ne soit réellement atteint. La question, telle que je l'ai comprise, était de savoir si l'UB pouvait apparaître si le code ne serait même pas atteint. Et bien sûr, la réponse est «non».
sepp2k
Eh bien, la norme n'est pas si claire à ce sujet dans 1.9 / 4, mais 1.9 / 5 peut éventuellement être interprété comme ce que vous avez dit.
Danvil
1
Les notes ne sont pas normatives. 1,9 / 5 l'emporte sur la note dans 1,9 / 4
MSalters
5

Un exemple instructif est

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Le GCC actuel et le Clang actuel optimiseront cela (sur x86) pour

xorl %eax,%eax
ret

car ils en déduisent xtoujours zéro de l'UB dans le if (x)chemin de contrôle. GCC ne vous donnera même pas d'avertissement d'utilisation d'une valeur non initialisée! (parce que la passe qui applique la logique ci-dessus s'exécute avant la passe qui génère des avertissements de valeur non initialisée)

zwol
la source
1
Exemple intéressant. C'est plutôt désagréable que l'activation de l'optimisation masque l'avertissement. Ce n'est même pas documenté - la documentation du GCC dit seulement que l'activation de l'optimisation produit plus d' avertissements.
sleske
@sleske C'est méchant, je suis d'accord, mais les avertissements de valeur non initialisés sont notoirement difficiles à "faire" - les faire parfaitement équivaut au problème d'arrêt, et les programmeurs deviennent étrangement irrationnels à propos de l'ajout d'initialisations de variables "inutiles" pour étouffer les faux positifs, donc les auteurs de compilateurs finissent par être au-dessus d'un baril. J'avais l'habitude de pirater GCC et je me souviens que tout le monde avait peur de jouer avec la passe d'avertissement de valeur non initialisée.
zwol
@zwol: Je me demande dans quelle mesure l '"optimisation" résultant d'une telle élimination du code mort rend le code utile plus petit, et combien finit par amener les programmeurs à agrandir le code (par exemple en ajoutant du code à initialiser amême si dans toutes les circonstances un non initialisé aserait passé à la fonction que la fonction ne ferait jamais rien avec elle)?
supercat
@supercat Je n'ai pas été profondément impliqué dans le travail du compilateur depuis environ 10 ans, et il est presque impossible de raisonner sur les optimisations à partir d'exemples de jouets. Ce type d'optimisation a tendance à être associé à une réduction globale de 2 à 5% de la taille du code sur des applications réelles, si je me souviens bien.
zwol
1
@supercat 2-5% est énorme alors que ces choses se passent. J'ai vu des gens transpirer 0,1%.
zwol
4

Le projet de travail actuel du C ++ indique dans la version 1.9.4 que

La présente Norme internationale n'impose aucune exigence sur le comportement des programmes qui contiennent un comportement non défini.

Sur cette base, je dirais qu'un programme contenant un comportement indéfini sur n'importe quel chemin d'exécution peut faire n'importe quoi à chaque fois de son exécution.

Il y a deux très bons articles sur le comportement indéfini et ce que font habituellement les compilateurs:

Jens
la source
1
Ça n'a aucun sens. La fonction int f(int x) { if (x > 0) return 100/x; else return 100; }n'invoque certainement jamais un comportement indéfini, même si elle 100/0n'est bien sûr pas définie.
fgp
1
@fgp Ce que dit la norme (en particulier 1.9 / 5), c'est que si un comportement non défini peut être atteint, peu importe quand il est atteint. Par exemple, il printf("Hello, World"); *((char*)NULL) = 0 n'est pas garanti d'imprimer quoi que ce soit. Cela facilite l'optimisation, car le compilateur peut librement réorganiser les opérations (soumises à des contraintes de dépendance, bien sûr) dont il sait qu'elles finiront par se produire, sans avoir à prendre en compte un comportement non défini.
fgp
Je dirais qu'un programme avec votre fonction ne contient pas de comportement indéfini, car il n'y a pas d'entrée où 100/0 sera évalué.
Jens
1
Exactement - donc ce qui compte, c'est de savoir si l'UB peut réellement être déclenché ou non, pas si elle peut théoriquement être déclenchée. Ou êtes-vous prêt à affirmer qu'il int x,y; std::cin >> x >> y; std::cout << (x+y);est permis de dire que "1 + 1 = 17", simplement parce qu'il y a des entrées où x+ydébordent (qui est UB puisque intest un type signé).
fgp
Formellement, je dirais que le programme a un comportement indéfini car il existe des entrées qui le déclenchent. Mais vous avez raison de dire que cela n'a pas de sens dans le contexte de C ++, car il serait impossible d'écrire un programme sans comportement indéfini. Je voudrais qu'il y ait moins de comportement indéfini en C ++, mais ce n'est pas ainsi que le langage fonctionne (et il y a quelques bonnes raisons à cela, mais elles ne concernent pas mon utilisation quotidienne ...).
Jens
3

Le mot «comportement» signifie que quelque chose est en train d'être fait . Un état qui n'est jamais exécuté n'est pas un «comportement».

Une illustration:

*ptr = 0;

Est-ce un comportement indéfini? Supposons que nous soyons sûrs à 100%ptr == nullptr à au moins une fois pendant l'exécution du programme. La réponse devrait être oui.

Et ça?

 if (ptr) *ptr = 0;

Est-ce indéfini? (Rappelez-vous ptr == nullptrau moins une fois?) J'espère bien que non, sinon vous ne pourrez pas du tout écrire de programme utile.

Aucun srandardese n'a été blessé dans l'élaboration de cette réponse.

n. «pronoms» m.
la source
3

Le comportement non défini survient lorsque le programme provoquera un comportement non défini, quoi qu'il arrive ensuite. Cependant, vous avez donné l'exemple suivant.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Sauf si le compilateur connaît la définition de PrintToConsole, il ne peut pas supprimer if (num == 3)conditionnel. Supposons que vous ayez LongAndCamelCaseStdio.hun en-tête système avec la déclaration suivante de PrintToConsole.

void PrintToConsole(int);

Rien de bien utile, d'accord. Maintenant, voyons à quel point le vendeur est mauvais (ou peut-être pas si mauvais, un comportement indéfini), en vérifiant la définition réelle de cette fonction.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Le compilateur doit en fait supposer que toute fonction arbitraire dont le compilateur ne sait pas ce qu'il fait peut quitter ou lancer une exception (dans le cas de C ++). Vous pouvez remarquer que *((char*)NULL) = 0;cela ne sera pas exécuté, car l'exécution ne continuera pas après l' PrintToConsoleappel.

Le comportement indéfini frappe lorsque PrintToConsole revient réellement. Le compilateur s'attend à ce que cela ne se produise pas (car cela amènerait le programme à exécuter un comportement indéfini quoi qu'il arrive), donc tout peut arriver.

Cependant, considérons autre chose. Disons que nous faisons une vérification nulle et que nous utilisons la variable après une vérification nulle.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

Dans ce cas, il est facile de remarquer que cela lol_null_checknécessite un pointeur non NULL. L'affectation à la warningvariable non volatile globale n'est pas quelque chose qui pourrait quitter le programme ou lever une exception. Le pointerest également non volatile, donc il ne peut pas changer sa valeur par magie au milieu de la fonction (si c'est le cas, c'est un comportement indéfini). Appellol_null_check(NULL) entraînera un comportement indéfini qui peut empêcher l'affectation de la variable (car à ce stade, le fait que le programme exécute le comportement non défini est connu).

Cependant, le comportement non défini signifie que le programme peut tout faire. Par conséquent, rien n'empêche le comportement non défini de remonter dans le temps et de planter votre programme avant la première ligne d' int main()exécutions. C'est un comportement indéfini, cela n'a pas à avoir de sens. Il peut tout aussi bien planter après avoir tapé 3, mais le comportement non défini remontera dans le temps et plantera avant même que vous ne tapiez 3. Et qui sait, peut-être qu'un comportement non défini écrasera la RAM de votre système, et fera planter votre système 2 semaines plus tard, pendant que votre programme non défini n'est pas en cours d'exécution.

Konrad Borowski
la source
Tous les points valides. PrintToConsoleest ma tentative d'insérer un effet secondaire externe au programme qui est visible même après un crash et est fortement séquencé. Je voulais créer une situation où nous pouvons dire avec certitude si cette déclaration a été optimisée. Mais vous avez raison de dire qu'il pourrait ne jamais revenir. Votre exemple d'écriture dans un global peut être soumis à d'autres optimisations qui ne sont pas liées à UB. Par exemple, un global inutilisé peut être supprimé. Avez-vous une idée pour créer un effet secondaire externe d'une manière qui garantit le retour du contrôle?
usr
Des effets secondaires observables dans le monde extérieur peuvent-ils être produits par du code qu'un compilateur serait libre d'assumer des retours? D'après ce que je comprends, même une méthode qui lit simplement une volatilevariable pourrait légitimement déclencher une opération d'E / S qui pourrait à son tour interrompre immédiatement le thread actuel; le gestionnaire d'interruption pourrait alors tuer le thread avant qu'il n'ait une chance d'effectuer autre chose. Je ne vois aucune justification par laquelle le compilateur pourrait pousser un comportement indéfini avant ce point.
supercat
Du point de vue de la norme C, il n'y aurait rien d'illégal à ce qu'un comportement indéfini oblige l'ordinateur à envoyer un message à certaines personnes qui traqueraient et détruiraient toutes les preuves des actions précédentes du programme, mais si une action pouvait mettre fin à un thread, alors tout ce qui est séquencé avant cette action devrait se produire avant tout comportement indéfini qui se produirait après.
supercat
1

Si le programme atteint une instruction qui invoque un comportement non défini, aucune exigence n'est imposée à la sortie / au comportement du programme. peu importe qu'ils aient lieu "avant" ou "après" un comportement non défini est invoqué.

Votre raisonnement sur les trois extraits de code est correct. En particulier, un compilateur peut traiter toute instruction invoquant de manière inconditionnelle un comportement indéfini comme GCC le traite __builtin_unreachable(): comme une indication d'optimisation que l'instruction est inaccessible (et par conséquent, que tous les chemins de code qui y mènent sans condition sont également inaccessibles). D'autres optimisations similaires sont bien entendu possibles.

R .. GitHub STOP AIDING ICE
la source
1
Par curiosité, quand a-t-il __builtin_unreachable()commencé à avoir des effets qui se sont déroulés à la fois en arrière et en avant dans le temps? Étant donné quelque chose comme extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }je pourrais voir le builtin_unreachable()comme étant bon de faire savoir au compilateur qu'il peut omettre l' returninstruction, mais ce serait plutôt différent de dire que le code précédent pourrait être omis.
supercat
@supercat puisque RESET_TRIGGER est volatile, l'écriture à cet emplacement peut avoir des effets secondaires arbitraires. Pour le compilateur, c'est comme un appel de méthode opaque. Par conséquent, il ne peut pas être prouvé (et ce n'est pas le cas) qui __builtin_unreachableest atteint. Ce programme est défini.
usr
@usr: Je pense que les compilateurs de bas niveau devraient traiter les accès volatils comme des appels de méthode opaques, mais ni clang ni gcc ne le font. Entre autres choses, un appel de méthode opaque pourrait entraîner l'écriture de tous les octets de à tout objet dont l'adresse a été exposée au monde extérieur, et qui n'a pas été ni ne sera accédé par un restrictpointeur en direct , à l'aide d'un unsigned char*.
supercat
@usr: Si un compilateur ne traite pas un accès volatil comme un appel de méthode opaque en ce qui concerne les accès aux objets exposés, je ne vois aucune raison particulière de m'attendre à ce qu'il le fasse à d'autres fins. La norme n'exige pas que les implémentations le fassent, car il existe certaines plates-formes matérielles sur lesquelles un compilateur peut être capable de connaître tous les effets possibles d'un accès volatile. Cependant, un compilateur adapté à une utilisation intégrée doit reconnaître que les accès volatils peuvent déclencher du matériel qui n'avait pas été inventé lors de l'écriture du compilateur.
supercat
@supercat Je pense que vous avez raison. Il semble que les opérations volatiles n'ont "aucun effet sur la machine abstraite" et ne peuvent donc pas terminer le programme ou provoquer des effets secondaires.
usr
1

De nombreuses normes pour de nombreux types de choses consacrent beaucoup d'efforts à décrire les choses que les implémentations DEVRAIENT ou NE DOIVENT PAS faire, en utilisant une nomenclature similaire à celle définie dans IETF RFC 2119 (sans nécessairement citer les définitions de ce document). Dans de nombreux cas, les descriptions des choses que les implémentations devraient faire sauf dans les cas où elles seraient inutiles ou peu pratiques sont plus importantes que les exigences auxquelles toutes les implémentations conformes doivent se conformer.

Malheureusement, les standards C et C ++ ont tendance à éviter les descriptions de choses qui, bien qu'elles ne soient pas obligatoires à 100%, devraient néanmoins être attendues d'implémentations de qualité qui ne documentent pas un comportement contraire. Une suggestion selon laquelle les implémentations devraient faire quelque chose pourrait être considérée comme impliquant que celles qui ne le sont pas sont inférieures, et dans les cas où il serait généralement évident quels comportements seraient utiles ou pratiques, par opposition à peu pratiques et inutiles, sur une implémentation donnée, il y avait il est peu perçu que la norme interfère avec ces jugements.

Un compilateur intelligent pourrait se conformer à la norme tout en éliminant tout code qui n'aurait aucun effet sauf lorsque le code reçoit des entrées qui provoqueraient inévitablement un comportement indéfini, mais «intelligent» et «stupide» ne sont pas des antonymes. Le fait que les auteurs de la norme aient décidé qu'il pourrait y avoir certains types de mises en œuvre où un comportement utile dans une situation donnée serait inutile et irréalisable n'implique aucun jugement sur la question de savoir si de tels comportements devraient être considérés comme pratiques et utiles pour d'autres. Si une mise en œuvre pouvait maintenir une garantie comportementale sans frais au-delà de la perte d'une opportunité d'élagage "de branche morte", presque toute valeur que le code utilisateur pourrait recevoir de cette garantie dépasserait le coût de sa fourniture. L'élimination des branches mortes peut être bien dans les cas où cela ne le serait pas, mais si, dans une situation donnée, le code utilisateur aurait pu gérer presque n'importe quel comportement possible autre que l'élimination des branches mortes, tout effort que le code utilisateur aurait à dépenser pour éviter UB dépasserait probablement la valeur obtenue à partir de DBE.

supercat
la source
C'est un bon point qu'éviter UB peut imposer un coût sur le code utilisateur.
usr
@usr: C'est un point que les modernistes manquent complètement. Dois-je ajouter un exemple? Par exemple, si le code doit évaluer le x*y < zmoment où x*yil ne déborde pas, et en cas de dépassement, le rendement est de 0 ou 1 de manière arbitraire mais sans effets secondaires, il n'y a aucune raison sur la plupart des plates-formes pour que le respect des deuxième et troisième exigences soit plus coûteux que répondre à la première, mais toute manière d'écrire l'expression pour garantir un comportement défini par Standard dans tous les cas entraînerait dans certains cas un coût important. Écrire l'expression comme (int64_t)x*y < zpourrait plus que quadrupler le coût de calcul ...
supercat
... sur certaines plates-formes, et l'écrire de manière à (int)((unsigned)x*y) < zempêcher un compilateur d'employer ce qui aurait pu être des substitutions algébriques utiles (par exemple, s'il sait cela xet zsont égaux et positifs, cela pourrait simplifier l'expression d'origine y<0, mais la version utilisant unsigned forcerait le compilateur à effectuer la multiplication). Si le compilateur peut garantir même si le Standard ne l'exige pas, il respectera l'exigence «rendement 0 ou 1 sans effets secondaires», le code utilisateur pourrait donner au compilateur des opportunités d'optimisation qu'il ne pourrait pas obtenir autrement.
supercat
Ouais, il semble qu'une forme plus douce de comportement non défini serait utile ici. Le programmeur peut activer un mode qui provoque l'émission x*yd'une valeur normale en cas de dépassement de capacité mais de n'importe quelle valeur. UB configurable en C / C ++ me semble important.
usr
@usr: Si les auteurs de la norme C89 étaient honnêtes en disant que la promotion de valeurs courtes non signées à la connexion était le changement de rupture le plus sérieux, et n'étaient pas des imbéciles ignorants, cela impliquerait qu'ils s'attendaient à cela dans les cas où les plates-formes avaient définissant des garanties comportementales utiles, les implémentations de ces plates-formes avaient mis ces garanties à la disposition des programmeurs et les programmeurs les avaient exploitées, les compilateurs pour ces plates-formes continueraient à offrir de telles garanties comportementales, que la norme leur ordonne ou non .
supercat