4
votes

Que se passerait-il si "i = i ++" n'était pas considéré comme un comportement non défini?

J'ai du mal à comprendre la différence entre un comportement non spécifié et un comportement non défini. Je pense qu'il serait utile d'essayer de comprendre certains exemples. Par exemple, x = x ++ . Le problème avec ce devoir est que:

Entre le point de séquence précédent et suivant, un objet doit avoir sa valeur stockée modifié au plus une fois par l'évaluation d'une expression. De plus, la valeur précédente doit être en lecture seule pour déterminer la valeur à stocker.

Ceci enfreint une règle shall, mais n'invoque pas explicitement un comportement indéfini, mais cela implique UB selon:

L'ordre d'évaluation des opérandes n'est pas spécifié. Si une tentative est faite pour modifier le résultat d'un opérateur d'affectation ou pour y accéder après le point de séquence suivant, le comportement n'est pas défini.

En supposant qu'aucune de ces règles n'existait et qu'il n'y ait pas d'autres règles qui "invalident" x = x ++ . La valeur de x serait alors non spécifiée, non?

Le doute a surgi parce que parfois on prétend que les choses en C sont UB par "défaut" ne sont valides que vous pouvez justifier que la construction est valide.

Modifier: comme indiqué par PW , il existe une version quelque peu connexe et bien reçue de cette question pour C ++: Qu'est-ce qui a fait i = i ++ + 1; légal en C ++ 17? .


15 commentaires

Le double est trop large, je ne pense pas qu'il devrait être utilisé pour clore cette question, qui est très précise. Je vais rouvrir.


«Indéfini» signifie que tout peut arriver, ou rien. Cela pourrait faire sortir les démons de votre nez, comme le dit l'un des aphorismes courants. «Non spécifié» signifie qu'il existe plusieurs alternatives, mais celle qui est spécifique n'est pas déterminée par la norme.


@usr Pour être honnête, cela m'a toujours laissé perplexe. Avec x = x ++ , il n'est pas clair si x se voit attribuer la valeur "ancienne" ou "nouvelle" (donc non spécifiée), mais c'est la seule ambiguïté que je puisse imaginer. Que pourrait-il se passer d'autre en créant des «démons volants» ou en faisant planter le programme?


@usr Si cela n'a pas été spécifié, si x est auparavant 1, alors 1 ou 2 après l'affectation, n'est-ce pas? Tbh, ma question s'est posée parce que certaines personnes disent que les constructions sont UB à moins que la norme ne dise le contraire. Mais je pense que les choses ne sont toujours pas spécifiées par défaut.


Le langage C # lui donne un comportement défini. Il le fait d'une manière simple, ne prend que 5 courtes lignes de texte. La chose évidente se produit lorsque vous l'utilisez, les membres de l'équipe cessent de vous parler ou vos requêtes git pull sont ignorées.


@HansPassant Donc, C ++ 17 gagne car ils lui ont donné un comportement défini avec seulement 2 lignes? :)


@Lundin C # définit toute une série d'autres choses dans ces 5 lignes courtes. C'est un tirage au sort.


@glglgl: C a été conçu pour être utilisé sur une vaste gamme d'équipements informatiques, y compris certaines machines très primitives. Dans certaines implémentations C, les types long ou même int peuvent être implémentés en les construisant à partir d'entiers plus étroits. Pour ces implémentations, x ++ n'est pas une seule opération. Cela peut nécessiter une incrémentation d'un octet, un test de report, etc. Ainsi, dans x = x ++; , le ++ n'est pas nécessairement avant ou après le = . Cela pourrait être en partie avant et en partie après. Le comité C a décidé de ne pas traiter de cela et de dire simplement que le comportement n'est pas défini.


@EricPostpischil Même cela, je peux comprendre. La valeur qui en résulte peut être n'importe quel garbage. Mais les effets de ceci devraient simplement affecter la valeur (ayant un résultat non spécifié) au lieu d'affecter l'ensemble du programme (ayant un comportement non défini).


@glglgl Si le résultat est une valeur de garbage, il peut s'agir d'une représentation d'interruption menant à UB. Je ne sais pas si c'est la seule raison, mais cela pourrait être au moins une.


@Ctx Oh, c'est logique. Je vous remercie.


@glglgl Un exemple d'UB serait int x = 0; x = x ++; si (x == 2) {do_stuff (); } , après quoi le compilateur est libre de supposer "aha, mais x n'est jamais égal à 2 pour que je puisse supprimer tout ce code". Et il est également gratuit de tout remplacer par do_stuff (); . Ou toute autre chose dont il pourrait avoir envie


@Lundin cela se lit comme des exemples de comportement non spécifié, pas de comportement non défini.


@Ctx Non, car on ne sait pas quelle valeur x pourrait réellement avoir ou ce que le programmeur voulait.


@glglgl: Les situations dans lesquelles certaines implémentations pourraient ne pas traiter le code de manière prévisible sont classées comme UB, sans se soucier de savoir si la plupart des implémentations traiteraient ce code de manière utile, ou même du fait que les versions précédentes du Standard avaient utilement défini le comportement du code. L'hypothèse est que les gens qui cherchent à écrire des compilateurs de qualité reconnaîtront qu'ils devraient traiter ce code de manière utile lorsque cela est possible, sans compter sur les auteurs de la norme pour leur dire cela.


5 Réponses :


2
votes

Le C11 / C17 moderne a changé le texte, mais il a à peu près la même signification. C17 6.5 / 2:

Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou à un calcul de valeur utilisant la valeur du même scalaire objet, le comportement n'est pas défini.

Il y a plusieurs problèmes légèrement différents ici, mélangés en un seul:

  • Entre les points de séquence, x est écrit plusieurs fois dans (effet secondaire). Ceci est UB comme indiqué ci-dessus.
  • Entre les points de séquence, l'expression contient au moins un effet secondaire et il y a un calcul de valeur de la même variable sans rapport avec la valeur à stocker. Il s'agit également de UB comme indiqué ci-dessus.
  • Dans l'expression x = x ++ , l'évaluation de l'opérande x n'est pas séquencée par rapport à l'opérande x ++ . L'ordre d'évaluation est un comportement non spécifié selon C17 6.5.16.

    L'effet secondaire de la mise à jour de la valeur stockée de l'opérande gauche est séquencé après les calculs de valeur des opérandes gauche et droit. Les évaluations de les opérandes ne sont pas séquencés.

Sinon pour la première partie citée étiquetant cet UB, alors nous ne saurions toujours pas si le x ++ serait séquencé avant ou après l'évaluation du x gauche opérande, il est donc difficile de raisonner sur la façon dont cela pourrait devenir "juste un comportement non spécifié".

C ++ 17 a en fait corrigé cette partie, la rendant bien définie ici, contrairement à C ++ ou aux versions antérieures de C ++. Ils l'ont fait en définissant l'ordre des séquences (C ++ 17 8.5.18):

Dans tous les cas, l'affectation est séquencée après la valeur calcul des opérandes droit et gauche, et avant le calcul de la valeur de l'expression d'affectation. L'opérande droit est séquencé avant l'opérande gauche.

Je ne vois pas comment il peut y avoir un terrain d'entente ici; soit l'expression n'est pas définie, soit elle est bien définie.


Un comportement non spécifié est un comportement déterministe dont nous ne pouvons rien savoir ou supposer. Mais contrairement au comportement indéfini, il ne provoquera pas de plantages et de comportement aléatoire du programme. Un bon exemple est a () + b () . Nous ne pouvons pas savoir quelle fonction sera exécutée en premier - le programme n'a même pas besoin d'être cohérent si la même ligne apparaît plus tard dans le même programme. Mais nous pouvons savoir que les deux fonctions seront exécutées l'une avant l'autre.

Contrairement à x = a () + b () + x ++; qui est un comportement non défini et nous ne pouvons rien supposer à ce sujet. L'une, les deux ou aucune des fonctions peuvent être exécutées, dans n'importe quel ordre. Le programme peut planter, produire des résultats incorrects, produire des résultats apparemment corrects ou ne rien faire du tout.


0 commentaires

2
votes

Il y a eu des cas dans d'autres langages de programmation où un comportement précédemment non défini a été défini dans une norme ultérieure. Un exemple dont je me souviens est en C ++ où ce qui était un comportement non défini en C ++ 11 est devenu bien défini en C ++ 17.

i = i++ + 1; // the behavior is undefined in C++11 

i = i++ + 1; // the behavior is well-defined in C++17. The value of i is incremented

Une question a été bien reçue sur ce sujet. Ce qui a rendu cela bien défini est une garantie dans la norme C ++ 17 que

L'opérande de droite est séquencé avant l'opérande de gauche.

Donc, dans un sens, il appartient aux membres du comité de normalisation de changer la norme et de fournir des garanties solides pour qu'elle soit bien définie.

Mais je ne pense pas que quelque chose d'aussi simple que x = x ++; sera rendu non spécifié. Il sera soit indéfini, soit bien défini.


0 commentaires

3
votes

J'ai du mal à comprendre la différence entre un comportement non spécifié et un comportement non défini.

Commençons ensuite par les définitions de ces termes de la norme:


Comportement de comportement indéfini , lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour lesquelles Norme internationale n'impose aucune exigence

REMARQUE Les comportements indéfinis possibles vont de l'ignorance totale de la situation avec des résultats imprévisibles, au comportement pendant traduction ou exécution de programme de manière documentée caractéristique de l'environnement (avec ou sans émission d'un diagnostic message), pour terminer une traduction ou une exécution (avec l'émission d'un message de diagnostic).

EXEMPLE Un exemple de comportement non défini est le comportement en cas de dépassement d'entier.

( C2011, 3.4.3 ) < / p>


comportement non spécifié utilisation d'une valeur non spécifiée, ou autre comportement lorsque la présente Norme internationale en fournit deux ou plus possibilités et n'impose aucune autre exigence sur laquelle est choisi dans tous les cas

EXEMPLE Un exemple de comportement non spécifié est l'ordre dans lequel le les arguments d'une fonction sont évalués.

( C2011, 3.4.4 ) < / p>


Vous remarquez que

Le doute a surgi parce que parfois on prétend que les choses en C sont UB par "défaut" ne sont valables que vous pouvez justifier que la construction est valide.

Il est peut-être exagéré de l'appeler argument, comme s'il y avait un doute sur sa validité. En vérité, il reflète un langage explicite de la norme:

Si une exigence «doit» ou «ne doit pas» qui apparaît en dehors d'un contrainte ou contrainte d'exécution est violée, le comportement est indéfini. Un comportement non défini est par ailleurs indiqué dans ce Norme internationale par les mots «comportement indéfini» ou par le omission de toute définition explicite du comportement . Il n'y a pas différence d'accent entre ces trois; ils décrivent tous '' le comportement qui n'est pas défini ''.

( C2011, 4/2 ; italiques ajoutés)

Quand vous posez

En supposant qu'aucune de ces règles n'existait et qu'il n'y ait pas d'autres règles qui "invalider" x = x ++.

, cela ne change rien nécessairement. En particulier, la suppression de la règle explicite selon laquelle l'ordre d'évaluation des opérandes n'est pas spécifié ne rend pas l'ordre spécifié . Je serais enclin à soutenir que l'ordre reste non spécifié, mais l'alternative est que le comportement ne serait pas défini. Le principal objectif poursuivi en disant explicitement que ce n'est pas spécifié est de contourner cette question.

La règle déclarant explicitement UB lorsqu'un objet est modifié deux fois entre les points de séquence est un peu moins claire, mais tombe dans le même bateau. On pourrait soutenir que la norme n'a toujours pas défini le comportement pour votre cas d'exemple, le laissant indéfini. Je pense que c'est un peu plus exagéré, mais c'est exactement pourquoi il est utile d'avoir une règle explicite, d'une manière ou d'une autre. Il serait possible de définir un comportement pour votre cas - Java le fait, par exemple - mais C choisit de ne pas le faire, pour diverses raisons techniques et historiques.

La valeur de x serait alors non spécifiée, n'est-ce pas?

Ce n'est pas tout à fait clair.

Veuillez comprendre également que les diverses dispositions de la norme ne sont pas pour la plupart isolées. Ils sont conçus pour fonctionner ensemble, comme un tout (principalement) cohérent. La suppression ou la modification de dispositions aléatoires présente un risque considérable de produire des incohérences ou des lacunes, ce qui rend difficile de raisonner sur le résultat.


4 commentaires

Faudrait-il alors mentionner explicitement le "comportement non spécifié"? J'ai compris que dans "autre comportement où la présente Norme internationale offre deux ou plusieurs possibilités", cela signifie que les règles de la Norme conduisent à plusieurs comportements possibles. Mais je commence à penser que les options doivent être explicitement énoncées.


@jinawee, Un comportement non spécifié exige que " [la] norme offre deux ou plusieurs possibilités". Il y a place à l'argumentation ici, mais je pense que c'est une condition plus forte que «deux ou plusieurs possibilités existent». Je ne pense pas que les mots magiques "il n'est pas spécifié" doivent nécessairement apparaître, par opposition à d'autres mots qui véhiculent la même idée, mais je pense que la norme doit énoncer des alternatives spécifiques et laisser explicitement le choix entre elles aux implémentations.


Merci. Les choses semblent plus cohérentes si, lorsqu'il y a plusieurs façons dont la machine à états abstraite peut évoluer et que le Standard ne dit rien, c'est UB au lieu d'un comportement non spécifié. Cela signifierait qu'un appel de fonction comme f (g1 (), g2 ()) serait UB si le Standard n'avait pas dit que l'ordre d'évaluation des arguments n'est pas spécifié. Ce qui est tout à fait raisonnable.


@jinawee: Un point clé qui apparaît clairement dans la justification de la norme publiée est que caractériser une action comme UB signifie simplement que les compilateurs qui ont une bonne raison de la traiter d'une manière particulière peuvent le faire; il ne nie en aucun cas l'existence d'un comportement banal (que les auteurs de la norme appelleraient une «extension populaire»). La prise en charge de telles "extensions populaires" est reconnue comme un problème de qualité de mise en œuvre, car les questions de savoir si une mise en œuvre a une "bonne raison" de faire quelque chose seraient mieux résolues par les personnes qui travaillent avec.



2
votes

Le problème semble ne pas pouvoir définir correctement ce que signifierait i = i ++; :

Interprétation 1:

    int i1= i;
    i = i1;
    int i2= i1+1;
    i= i2;

Dans ce interprétation la valeur de i est récupérée et 1 est ajouté ( i2 ), puis ce i2 est enregistré dans i mais le i d'origine dans i1 est ensuite utilisé dans l'affectation (car ici le ++ est interprété pour s'appliquer à la valeur après utilisé) et donc i est inchangé.

Interprétation 2:

    int i1= i;
    i1= i1+1;
    i= i1;
    int i2= i;
    i= i2;

Dans cette interprétation, le i ++ code> est exécuté en premier (et modifie i ) et maintenant le i modifié est à nouveau récupéré et utilisé dans l'affectation (donc i a le valeur).

Interprétation 3:

    int i1= i;
    int i2= i1+1;
    i = i2;
    i = i1;

Dans cette interprétation d'abord, l'affectation de i à i code> est exécuté puis i est incrémenté.

Pour moi, ces trois interprétations sont correctes , et il pourrait même y avoir quelques interprétations supplémentaires, mais chacune fait quelque chose de différent. Par conséquent, le standard pourrait / ne l'a pas défini et quelle interprétation un compilateur utilise appartient au constructeur du compilateur et par conséquent, quel comportement un compilateur présente est indéfini: comportement indéfini.

(Un compilateur pourrait même générer un jmp toTheMoon ou ignorez toute l'instruction.)


0 commentaires

1
votes

L'ordre d'évaluation et d'application de l'effet secondaire de ++ est laissé non spécifié - la norme de langage ne prescrit pas de gauche à droite ou de droite à- ordre de gauche (pour les opérateurs arithmétiques, de toute façon). Considérez l'expression bien définie a = b ++ * ++ c . Les expressions a , b ++ et ++ c peuvent être évaluées dans n'importe quel ordre. De même, les effets secondaires de b et c peuvent être appliqués immédiatement après l'évaluation, ou différés juste avant le point de séquence suivant, ou n'importe où entre les deux. Tout ce qui compte, c'est que le résultat de b * (c + 1) soit calculé avant d'être affecté à a . Voici une évaluation parfaitement légale:

Option 1         Option 2
--------         --------
tmp <- x         tmp <- x
x <- x + 1       x <- tmp
x <- tmp         x <- x + 1

Alors est-ce:

tmp1 <- b
b <- b + 1
tmp2 <- c + 1
a <- tmp1 * tmp2
c <- c + 1

Alors est-ce:

c <- c + 1
a <- b * c
b <- b + 1

Ce qui compte, c'est que, quel que soit l'ordre d'évaluation choisi, vous obtiendrez toujours le même résultat.

x = x ++ pourrait être évalué de l'une des manières suivantes, selon le moment où l'effet secondaire est appliqué:

tmp <- c + 1;
a = b * tmp;
c <- c + 1
b <- b + 1

Le problème est que les deux méthodes donnent des résultats différents . D'autres méthodes complètement différentes peuvent être disponibles en fonction du jeu d'instructions qui donnent des résultats différents de ceux-ci.

Le standard de langage n'impose pas ce qu'il faut faire lorsqu'une expression donne des résultats différents selon l'ordre dans lequel elle est évaluée - il ne place aucune exigence sur le compilateur ou l'environnement d'exécution pour choisir l'un ou l'autre option. C'est ce que signifie undefined - littéralement, le comportement n'est pas défini par la spécification du langage. Vous obtiendrez un résultat, mais il n'est pas garanti d'être cohérent, ni le résultat que vous attendez.

Indéfini ne signifie pas illégal . Cela ne signifie pas non plus que votre code risque de planter. Cela signifie simplement que le résultat n'est pas prévisible ou garanti cohérent. Une implémentation n'a même pas besoin d'émettre un diagnostic disant "hé, factice, c'est une mauvaise idée."

Une implémentation est libre de définir et de documenter un comportement non défini par le standard (comme MSVC définition de fflush sur les flux d'entrée). Un certain nombre de compilateurs tirent parti du fait que certains comportements ne sont pas définis pour effectuer certaines optimisations. Et certains compilateurs émettent des avertissements pour les erreurs courantes telles que x = x ++ .


1 commentaires

Mon raisonnement était le suivant: il y a deux options possibles, il s'agit donc d'un comportement non spécifié et le compilateur peut choisir comment l'implémenter. Mais il est beaucoup plus logique de le comprendre comme suit: il y a deux options et la norme ne dit pas que c'est un comportement non spécifié, donc c'est UB par défaut. Bien sûr, il est plus logique que la norme indique explicitement son UB afin qu'il n'y ait pas de place pour la confusion.