web-dev-qa-db-fra.com

Le "comportement indéfini" permet-il vraiment que * quelque chose * se produise?

EDIT: Cette question n'a pas été conçue comme un forum de discussion sur les (dé) mérites d'un comportement indéfini, mais c'est en quelque sorte ce qu'il est devenu. Dans tous les cas, ce fil sur un compilateur C hypothétique sans comportement indéfini peut être d'un intérêt supplémentaire pour ceux qui pensent que c'est un sujet important.


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

Parce que les communautés C et C++ ont tendance à mettre un tel accent sur l'imprévisibilité d'un comportement indéfini et l'idée que le compilateur est autorisé à faire faire au programme littéralement n'importe quoi lorsqu'un comportement indéfini est rencontré, je avait supposé que la norme n'imposait aucune restriction sur le comportement, enfin, d'un comportement non défini.

Mais le la citation pertinente dans la norme C++ semble être :

[C++14: defns.undefined]: [..] Le comportement non défini autorisé va de l'ignorance complète 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 caractéristique de l'environnement (avec ou sans émission de un message de diagnostic), pour terminer une traduction ou une exécution (avec émission d'un message de diagnostic). [..]

Cela spécifie en fait un petit ensemble d'options possibles:

  • Ignorer la situation - Oui, la norme continue en disant que cela aura des "résultats imprévisibles", mais ce n'est pas la même chose que le compilateur inserting code (qui, je suppose, serait une condition préalable pour, vous savez, les démons nasaux).
  • Comportement documenté 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.)
  • Fin de la traduction ou de l'exécution - avec un diagnostic, rien de moins. Est-ce que tout UB se comporterait si bien?.

Je suppose que dans la plupart des cas, les compilateurs choisissent d'ignorer le comportement indéfini; par exemple, lors de la lecture de la mémoire non initialisée, ce serait vraisemblablement une anti-optimisation d'insérer n'importe quel code pour garantir un comportement cohérent. Je suppose que les types inconnus de comportements indéfinis (tels que " voyage dans le temps ") relèveraient de la deuxième catégorie - mais cela nécessite 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?).

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

EDIT: Deux points mineurs de clarification:

  • Je pensais que c'était clair à partir de la question d'origine, et je pense que c'était le cas pour la plupart des gens, mais je l'expliquerai quand même: je me rends compte que les "démons nasaux" sont ironiques.
  • Veuillez ne pas écrire une (autre) réponse expliquant que UB permet des optimisations de compilateur spécifiques à la plate-forme, sauf si vous aussi expliquez comment il permet des optimisations qui définies par l'implémentation = comportement ne serait pas autoriser.
93
Kyle Strand

Oui, cela permet que tout se passe. La note ne donne que des exemples. La définition est assez claire:

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


Point de confusion fréquent:

Vous devez comprendre que "aucune exigence" signifie également signifie que la mise en œuvre est PAS 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++ pour documenter certains comportements sains et se comporter en conséquence.1 Donc, si votre compilateur prétend contourner le débordement signé, la logique (raison?) Dicterait que vous êtes le bienvenu à compter sur ce comportement sur ce compilateur . Ne vous attendez pas à ce qu'un autre compilateur se comporte de la même manière s'il ne le prétend pas.

1Heck, il est même permis de documenter une chose et d'en faire une autre. Ce serait stupide, et cela vous inciterait probablement à le jeter à la poubelle - pourquoi feriez-vous confiance à un compilateur dont la documentation vous appartient? - mais ce n'est pas contraire à la norme C++.

76
Mehrdad

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

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

certains compilateurs pourraient garantir que le code imprimerait une valeur particulière (pour une machine à complément à deux, il s'agirait généralement de INT_MIN), tandis que d'autres garantiraient 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ù la fin anormale d'un programme était une conséquence acceptable du débordement mais produisant une sortie apparemment valide mais erronée ne le serait pas, pourrait renoncer à la vérification du débordement si elle était exécutée sur une plate-forme qui le bloquerait de manière fiable, et une application où une interruption anormale en cas de débordement ne serait pas acceptable, mais produirait une sortie arithmétiquement incorrecte, pourrait renoncer à la vérification du débordement si elle était exécutée sur une plate-forme où les débordements n'étaient pas bloqués.

Récemment, cependant, certains auteurs de compilateurs semblent être entrés dans un concours pour voir qui peut éliminer le plus efficacement tout code dont l'existence ne serait pas prescrite 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 74 ou plus, le calcul de ch*ch*ch*ch*ch produirait un comportement indéfini et, par conséquent, le programme devrait afficher "Hey there!" sans condition quel que soit le caractère tapé.

23
supercat

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

Ce sont les sources utilisées pour générer les brouillons de la norme C++. Ces sources ne doivent pas être considérées comme une publication ISO, pas plus que les documents générés à partir de celles-ci sauf s'ils ont été officiellement adoptés par le groupe de travail C++ (ISO/IEC JTC1/SC22/WG21).

Interprétation : Les notes ne sont pas normatives selon les Directives ISO/CEI Partie 2.

Les notes et exemples intégrés dans le texte d'un document ne doivent être utilisés que pour fournir des informations supplémentaires destinées à faciliter la compréhension ou l'utilisation du document. Ils ne doivent contenir aucune exigence ("doit"; voir 3.3.1 et tableau H.1) ni aucune information jugée indispensable pour l'utilisation du document , par exemple instructions (impératif; voir le tableau H.1), recommandations ("devrait"; voir 3.3.2 et tableau H.2) ou autorisation ("peut"; voir le tableau H.3). Les notes peuvent être rédigées comme un énoncé de fait.

Je souligne. Cela exclut à lui seul une "liste complète des options". Donner des exemples compte cependant comme "des informations supplémentaires destinées à faciliter la compréhension .. du document".

Gardez à l'esprit que le mème "démon nasal" n'est pas censé être pris à la lettre, tout comme l'utilisation d'un ballon pour expliquer comment fonctionne l'expansion de l'univers ne contient aucune vérité dans la réalité physique. C'est pour illustrer qu'il est imprudent de discuter de ce qu'un "comportement indéfini" devrait faire quand il est permis de faire quoi que ce soit. Oui, cela signifie qu'il n'y a pas de véritable bande élastique dans l'espace.

15
user5250294

La définition d'un comportement indé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 tout résultat est autorisé. Mais il n'y a pas de résultats particuliers qui requis se produisent, ni aucun résultat qui --- requis ne se produise PAS. Peu importe si vous avez un compilateur et une bibliothèque qui produisent systématiquement un comportement particulier en réponse à une instance particulière de comportement non défini - un tel comportement n'est pas requis, et peut même changer 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 prend en charge le matériel sous la forme d'une connexion à des sondes insérées dans vos narines, il est possible que l'apparition d'un comportement non défini provoque des effets nasaux indésirables.

11
Peter

J'ai pensé répondre à un seul de vos points, car les autres réponses répondent assez bien à la question générale, mais je n'ai pas répondu à cette question.

"Ignorer la situation - Oui, la norme continue en disant que cela aura" des résultats imprévisibles ", mais ce n'est pas la même chose que le code d'insertion du compilateur (qui, je suppose, serait une condition préalable pour, vous savez, les démons nasaux). "

Une situation dans laquelle on pourrait très raisonnablement s'attendre à ce que des démons nasaux se produisent avec un compilateur raisonnable, sans que le compilateur n'insère AUCUN 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, s'il peut prouver que * x est une déréférence de pointeur nul, a parfaitement le droit, dans le cadre d'une optimisation, de dire "OK, donc je vois qu'ils ont déréférencé un pointeur nul dans cette branche de l'if. Par conséquent, dans le cadre de cette branche, je suis autorisé à faire n'importe quoi. Je peux donc optimiser pour cela: "

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

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

nasal_demons();

Vous pouvez voir comment ce genre de chose peut, dans les bonnes circonstances, s'avérer très utile pour un compilateur d'optimisation, tout en provoquant un désastre. J'ai vu quelques exemples il y a quelque temps de cas où en fait il est IS important pour l'optimisation de pouvoir optimiser ce genre de cas. Je pourrais essayer de les creuser plus tard quand j'aurai plus de temps.

EDIT: Un exemple qui vient des profondeurs de ma mémoire d'un tel cas où il est utile pour l'optimisation est où vous vérifiez très fréquemment qu'un pointeur est NULL (peut-être dans des fonctions d'assistance intégrées), même après l'avoir déjà déréférencé et sans avoir changé. Le compilateur d'optimisation peut voir que vous l'avez déréférencé et ainsi optimiser toutes les vérifications "is NULL", car si vous l'avez déréférencé et qu'il IS null, tout est autorisé à se produire, y compris le fait de ne pas exécuter les vérifications "is NULL". Je pense que des arguments similaires s'appliquent à d'autres comportements non définis.

8
Muzer

Tout d'abord, il est important de noter que ce n'est pas seulement le comportement du programme utilisateur qui n'est pas défini, c'est le comportement du compilateur que n'est pas défini . De même, UB n'est pas rencontré à l'exécution, c'est une propriété du code source.

Pour un rédacteur de compilateur, "le comportement n'est pas défini" signifie "vous n'avez pas à prendre cette situation en compte" ou même "vous pouvez supposer qu'aucun code source ne produira jamais cette situation". Un compilateur peut faire n'importe quoi, intentionnellement ou non, lorsqu'il est présenté avec UB, et être toujours conforme aux normes, alors oui, si vous avez accordé l'accès à votre nez ...

Ensuite, il n'est pas toujours possible de savoir si un programme a UB ou non. Exemple:

int * ptr = calculateAddress();
int i = *ptr;

Savoir si cela peut être UB ou non nécessiterait de connaître toutes les valeurs possibles renvoyées par calculateAddress(), ce qui est impossible dans le cas général (Voir " Halting Problem "). Un compilateur a deux choix:

  • supposons que ptr aura toujours une adresse valide
  • insérer des contrôles d'exécution pour garantir un certain comportement

La première option produit des programmes rapides et met le fardeau d'éviter les effets indésirables sur le programmeur, tandis que la deuxième option produit un code plus sûr mais plus lent.

Les normes C et C++ laissent ce choix ouvert, et la plupart des compilateurs choisissent le premier, tandis que Java par exemple rend obligatoire le second).


Pourquoi le comportement n'est-il pas défini par l'implémentation, mais non défini?

Défini par l'implémentation signifie ( N4296 , 1.9§2):

Certains aspects et opérations de la machine abstraite sont décrits dans la présente Norme internationale comme définis par l'implémentation (par exemple, sizeof (int)). Ceux-ci constituent les paramètres de la machine abstraite. Chaque mise en œuvre doit comprendre une documentation décrivant ses caractéristiques et son comportement à ces égards. Cette documentation doit définir l'instance de la machine abstraite qui correspond à cette implémentation (appelée ci-dessous "l'instance correspondante").

Je souligne. En d'autres termes: un compilateur-rédacteur doit documenter exactement comment se comporte le code machine, lorsque le code source utilise des fonctionnalités définies par l'implémentation.

L'écriture dans un pointeur non valide aléatoire non nul est l'une des choses les plus imprévisibles que vous puissiez faire dans un programme, ce qui nécessiterait également des vérifications d'exécution réduisant les performances.
Avant d'avoir des MMU, vous pouviez détruire le matériel en écrivant à la mauvaise adresse, qui vient très proche des démons nasaux ;-)

6
alain

L'une des raisons pour lesquelles le comportement n'est pas défini est de permettre au compilateur de faire les hypothèses qu'il souhaite lors de l'optimisation.

S'il existe une condition qui doit être respectée si une optimisation doit être appliquée et que cette condition dépend d'un comportement non défini dans le code, le compilateur peut supposer qu'elle est remplie, car un programme conforme ne peut dépendre d'aucun comportement non défini façon. Surtout, le compilateur n'a pas besoin d'être cohérent dans ces hypothèses. (ce qui est pas le cas pour un comportement défini par l'implémentation)

Supposons donc que votre code contienne un exemple artificiel comme celui ci-dessous:

int bar = 0;
int foo = (undefined behavior of some kind);
if (foo) {
   f();
   bar = 1;
}
if (!foo) {
   g();
   bar = 1;
}
assert(1 == bar);

Le compilateur est libre de supposer que! Foo est vrai dans le premier bloc et foo est vrai dans le second, et optimise ainsi tout le morceau de code. Maintenant, logiquement, foo ou! Foo doit être vrai, et donc en regardant le code, vous pourriez raisonnablement supposer que la barre doit être égale à 1 une fois que vous avez exécuté le code. Mais parce que le compilateur est optimisé de cette manière, la barre n'est jamais définie sur 1. Et maintenant, cette assertion devient fausse et le programme se termine, ce qui ne se serait pas produit si foo ne s'était pas appuyé sur un comportement non défini.

Maintenant, est-il possible que le compilateur insère réellement du code complètement nouveau s'il voit un comportement non défini? Si cela lui permet d'optimiser davantage, absolument. Est-ce susceptible de se produire souvent? Probablement pas, mais vous ne pouvez jamais le garantir, donc opérer sur l'hypothèse que les démons nasaux sont possibles est la seule approche sûre.

4
Ray

Un comportement indéfini est simplement le résultat d'une situation à venir que les rédacteurs de la spécification n'avaient pas prévue.

Prenez l'idée d'un feu de circulation. Le rouge signifie arrêter, le jaune signifie se préparer au rouge et le vert signifie partir. Dans cet exemple, les personnes conduisant des voitures sont la mise en œuvre de la spécification.

Que se passe-t-il si le vert et le rouge sont allumés? Arrêtez-vous, puis partez? Attendez-vous que le rouge s'éteigne et qu'il soit juste vert? Il s'agit d'un cas que la spécification n'a pas décrit et, par conséquent, tout ce que font les pilotes est un comportement indéfini. Certaines personnes feront une chose, d'autres une autre. Puisqu'il n'y a aucune garantie sur ce qui va se passer, vous voulez éviter cette situation. La même chose s'applique au code.

3
Waters

Des comportements non définis permettent aux compilateurs de générer du code plus rapidement dans certains cas. Considérons deux architectures de processeur différentes qui s'ajoutent différemment: le processeur A rejette de manière inhérente le bit de retenue lors d'un débordement, tandis que le processeur B génère une erreur. (Bien sûr, le processeur C génère intrinsèquement des démons nasaux - c'est juste le moyen le plus simple de décharger ce peu d'énergie supplémentaire dans un nanobot alimenté par la morve ...)

Si la norme exigeait qu'une erreur soit générée, alors tout le code compilé pour le processeur A serait fondamentalement obligé d'inclure des instructions supplémentaires, d'effectuer une sorte de vérification du débordement et, si oui, de générer une erreur. Cela entraînerait un code plus lent, même si le développeur savait qu'il n'allait finir par ajouter de petits nombres.

Un comportement indéfini sacrifie la portabilité pour la vitesse. En permettant à "n'importe quoi" de se produire, le compilateur peut éviter d'écrire des contrôles de sécurité pour des situations qui ne se produiront jamais. (Ou, vous savez ... ils pourraient.)

De plus, lorsqu'un programmeur sait exactement ce qu'un comportement indéfini entraînera réellement dans son environnement donné, il est libre d'exploiter ces connaissances pour obtenir des performances supplémentaires.

Si vous souhaitez vous assurer que votre code se comporte exactement de la même manière sur toutes les plates-formes, vous devez vous assurer qu'aucun "comportement indéfini" ne se produit jamais - cependant, ce n'est peut-être pas votre objectif.

Edit: (En réponse à l'OPs edit) Implémentation Un comportement défini nécessiterait la génération cohérente de démons nasaux. Un comportement indéfini permet la génération sporadique de démons nasaux.

C'est là que l'avantage d'un comportement non défini sur un comportement spécifique à l'implémentation apparaît. Considérez qu'un code supplémentaire peut être nécessaire pour éviter un comportement incohérent sur un système particulier. Dans ces cas, un comportement non défini permet une plus grande vitesse.

1
Allen