Je lisais sur l' ordre des violations d'évaluation , et ils donnent un exemple qui me laisse perplexe.
1) Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un autre effet secondaire sur le même objet scalaire, le comportement n'est pas défini.
// snip f(i = -1, i = -1); // undefined behavior
Dans ce contexte, i
est un objet scalaire , ce qui signifie apparemment
Les types arithmétiques (3.9.1), les types d'énumération, les types pointeur, le pointeur sur les types membres (3.9.2), std :: nullptr_t et les versions qualifiées cv de ces types (3.9.3) sont collectivement appelés types scalaires.
Je ne vois pas en quoi la déclaration est ambiguë dans ce cas. Il me semble que peu importe si le premier ou le deuxième argument est évalué en premier, i
finit par -1
, et les deux arguments le sont également -1
.
Quelqu'un peut-il clarifier?
METTRE À JOUR
J'apprécie vraiment toute la discussion. Jusqu'à présent, j'aime beaucoup la réponse de @ harmic car elle expose les pièges et les subtilités de la définition de cette déclaration malgré sa simplicité à première vue. @ acheong87 souligne certains problèmes qui surviennent lors de l'utilisation de références, mais je pense que c'est orthogonal à l'aspect des effets secondaires non séquencés de cette question.
RÉSUMÉ
Étant donné que cette question a retenu l'attention, je vais résumer les principaux points / réponses. Tout d'abord, permettez-moi de faire une petite digression pour souligner que "pourquoi" peut avoir des significations étroitement liées mais subtilement différentes, à savoir "pour quelle cause ", "pour quelle raison " et "dans quel but ". Je vais regrouper les réponses par lesquelles de ces significations de «pourquoi» elles ont abordées.
pour quelle cause
La réponse principale ici vient de Paul Draper , avec Martin J apportant une réponse similaire mais pas aussi complète. La réponse de Paul Draper se résume à
Il s'agit d'un comportement indéfini car il n'est pas défini quel est le comportement.
La réponse est globalement très bonne pour expliquer ce que dit la norme C ++. Il aborde également certains cas connexes d'UB tels que f(++i, ++i);
et f(i=1, i=-1);
. Dans le premier des cas connexes, il n'est pas clair si le premier argument doit être i+1
et le second i+2
ou vice versa; dans le second, il n'est pas clair si i
doit être 1 ou -1 après l'appel de fonction. Ces deux cas sont UB car ils relèvent de la règle suivante:
Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un autre effet secondaire sur le même objet scalaire, le comportement n'est pas défini.
Par conséquent, f(i=-1, i=-1)
est également UB car il relève de la même règle, malgré que l'intention du programmeur soit (à mon humble avis) évidente et sans ambiguïté.
Paul Draper précise également dans sa conclusion que
Serait-ce un comportement défini? Oui. At-il été défini? Non.
ce qui nous amène à la question de "pour quelle raison / but a été f(i=-1, i=-1)
laissé comme comportement indéfini?"
pour quelle raison / but
Bien qu'il y ait quelques oublis (peut-être négligents) dans la norme C ++, de nombreuses omissions sont bien motivées et servent un objectif spécifique. Bien que je sois conscient que le but est souvent soit de "faciliter le travail du compilateur-rédacteur", soit de "code plus rapide", j'étais surtout intéressé de savoir s'il y avait une bonne raison de quitter f(i=-1, i=-1)
UB.
harmic et supercat fournissent les principales réponses qui fournissent une raison pour l'UB. Harmic souligne qu'un compilateur d'optimisation qui pourrait diviser les opérations d'affectation apparemment atomiques en plusieurs instructions machine, et qu'il pourrait encore entrelacer ces instructions pour une vitesse optimale. Cela pourrait conduire à des résultats très surprenants: i
finit comme -2 dans son scénario! Ainsi, harmic montre comment l'attribution de la même valeur à une variable plus d'une fois peut avoir des effets néfastes si les opérations ne sont pas séquencées.
supercat fournit une exposition connexe des embûches d'essayer f(i=-1, i=-1)
de faire ce qu'il semble devoir faire. Il souligne que sur certaines architectures, il existe des restrictions strictes contre plusieurs écritures simultanées vers la même adresse mémoire. Un compilateur pourrait avoir du mal à comprendre cela si nous avions affaire à quelque chose de moins trivial que f(i=-1, i=-1)
.
davidf fournit également un exemple d'instructions d'entrelacement très similaires à celles d'Harmic.
Bien que chacun des exemples de Harmic, Supercat et Davidf soit quelque peu artificiel, pris ensemble, ils servent toujours à fournir une raison tangible pour laquelle f(i=-1, i=-1)
un comportement devrait être indéfini.
J'ai accepté la réponse d'Harmic parce qu'elle a fait le meilleur travail pour aborder toutes les significations du pourquoi, même si la réponse de Paul Draper abordait mieux la partie "pour quelle cause".
autres réponses
JohnB souligne que si nous considérons des opérateurs d'affectation surchargés (au lieu de simples scalaires), nous pouvons également rencontrer des problèmes.
la source
std::nullptr_t
et les versions qualifiées cv de ces types (3.9.3) sont collectivement appelés types scalaires . "f(i-1, i = -1)
ou quelque chose de similaire.Réponses:
Les opérations n'étant pas séquencées, rien ne permet de dire que les instructions d'exécution de la mission ne peuvent pas être entrelacées. Il peut être optimal de le faire, selon l'architecture du processeur. La page référencée indique ceci:
En soi, cela ne semble pas poser de problème - en supposant que l'opération effectuée stocke la valeur -1 dans un emplacement mémoire. Mais rien ne dit non plus que le compilateur ne peut pas optimiser cela en un ensemble d'instructions distinct qui a le même effet, mais qui pourrait échouer si l'opération était entrelacée avec une autre opération sur le même emplacement mémoire.
Par exemple, imaginez qu'il était plus efficace de mettre à zéro la mémoire, puis de la décrémenter, par rapport au chargement de la valeur -1 in. Ensuite, ceci:
pourrait devenir:
Maintenant, je suis -2.
C'est probablement un faux exemple, mais c'est possible.
la source
load 8bit immediate and shift
jusqu'à 4 fois. Habituellement, le compilateur fera un adressage indirect pour récupérer un nombre à partir d'une table pour éviter cela. (-1 peut être fait en 1 instruction, mais un autre exemple pourrait être choisi).Tout d'abord, "objet scalaire" signifie un type comme un
int
,float
ou un pointeur (voir Qu'est - ce qu'un objet scalaire en C ++? ).Deuxièmement, il peut sembler plus évident que
aurait un comportement indéfini. Mais
est moins évident.
Un exemple légèrement différent:
Quelle affectation s'est produite "en dernier"
i = 1
, oui = -1
? Ce n'est pas défini dans la norme. Vraiment, ce moyeni
pourrait l'être5
(voir la réponse de Harmic pour une explication complètement plausible de la façon dont cela pourrait être le cas). Ou votre programme pourrait segfault. Ou reformatez votre disque dur.Mais maintenant, vous demandez: "Qu'en est-il de mon exemple? J'ai utilisé la même valeur (
-1
) pour les deux affectations. Qu'est-ce qui pourrait ne pas être clair à ce sujet?"Vous avez raison ... sauf dans la façon dont le comité des normes C ++ l'a décrit.
Ils auraient pu faire une exception spéciale pour votre cas particulier, mais ils ne l'ont pas fait. (Et pourquoi devraient-ils? Quelle utilité cela pourrait-il jamais avoir?) Donc, cela
i
pourrait encore être5
. Ou votre disque dur peut être vide. Ainsi, la réponse à votre question est:Il s'agit d'un comportement indéfini car il n'est pas défini quel est le comportement.
(Cela mérite d'être souligné car de nombreux programmeurs pensent que "non défini" signifie "aléatoire" ou "imprévisible". Ce n'est pas le cas; cela signifie qu'il n'est pas défini par la norme. Le comportement pourrait être cohérent à 100% et toujours indéfini.)
Serait-ce un comportement défini? Oui. At-il été défini? Non. Par conséquent, il est "indéfini".
Cela dit, "non défini" ne signifie pas qu'un compilateur formatera votre disque dur ... cela signifie qu'il le pourrait et que ce serait toujours un compilateur conforme aux normes. De manière réaliste, je suis sûr que g ++, Clang et MSVC feront tous ce que vous attendiez. Ils n'auraient tout simplement pas "à le faire".
Une autre question pourrait se poser: pourquoi le comité des normes C ++ a-t-il choisi de ne pas séquencer cet effet secondaire? . Cette réponse portera sur l'histoire et les opinions du comité. Ou à quoi bon avoir cet effet secondaire non séquencé en C ++? , qui permet toute justification, qu'il s'agisse ou non du raisonnement réel du comité des normes. Vous pouvez poser ces questions ici ou sur programmers.stackexchange.com.
la source
-Wsequence-point
pour g ++, il vous avertira.undefined behavior
moyenssomething random will happen
sont loin d'être le cas la plupart du temps.Une raison pratique de ne pas faire d'exception aux règles simplement parce que les deux valeurs sont les mêmes:
Considérez le cas où cela a été autorisé.
Maintenant, quelques mois plus tard, le besoin se fait sentir de changer
Apparemment inoffensif, n'est-ce pas? Et pourtant soudainement prog.cpp ne compilerait plus. Pourtant, nous pensons que la compilation ne devrait pas dépendre de la valeur d'un littéral.
Conclusion: il n'y a pas d'exception à la règle car cela rendrait la compilation réussie dépendante de la valeur (plutôt du type) d'une constante.
ÉDITER
@HeartWare a souligné que les expressions constantes du formulaire
A DIV B
ne sont pas autorisées dans certaines langues, quandB
est 0, et entraînent l'échec de la compilation. Par conséquent, le changement d'une constante peut entraîner des erreurs de compilation à un autre endroit. Ce qui est, à mon humble avis, malheureux. Mais il est certainement bon de restreindre ces choses à l'inévitable.la source
f(i = VALUEA, i = VALUEB);
a certainement le potentiel d'un comportement indéfini. J'espère que vous ne codez pas vraiment contre les valeurs derrière les identifiants.SomeProcedure(A, B, B DIV (2-A))
. Quoi qu'il en soit, si le langage stipule que CONST doit être entièrement évalué au moment de la compilation, alors, bien sûr, ma réclamation n'est pas valable pour ce cas. Puisqu'il brouille en quelque sorte la distinction entre temps de compilation et temps d'exécution. Serait-il également remarqué si nous écrivonsCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
?? Ou les fonctions ne sont-elles pas autorisées?La confusion est que le stockage d'une valeur constante dans une variable locale n'est pas une instruction atomique sur chaque architecture sur laquelle le C est conçu pour être exécuté. Le processeur sur lequel le code fonctionne est plus important que le compilateur dans ce cas. Par exemple, sur ARM où chaque instruction ne peut pas transporter une constante complète de 32 bits, le stockage d'un entier dans une variable nécessite plus d'une instruction. Exemple avec ce pseudo code où vous ne pouvez stocker que 8 bits à la fois et devez travailler dans un registre 32 bits, i est un int32:
Vous pouvez imaginer que si le compilateur veut l'optimiser, il peut entrelacer deux fois la même séquence, et vous ne savez pas quelle valeur sera écrite dans i; et disons qu'il n'est pas très intelligent:
Cependant, dans mes tests, gcc est assez gentil pour reconnaître que la même valeur est utilisée deux fois et la génère une fois et ne fait rien de bizarre. Je reçois -1, -1 Mais mon exemple est toujours valable car il est important de considérer que même une constante peut ne pas être aussi évidente qu'elle semble l'être.
la source
-1
(que le compilateur a stocké quelque part), mais c'est plutôt3^81 mod 2^32
, mais constant, alors le compilateur peut faire exactement ce qui est fait ici, et dans un levier d'omtimisation, j'entrelace les séquences d'appels pour éviter d'attendre.f(i = A, j = B)
oùi
etj
sont deux objets distincts. Cet exemple n'a pas d'UB. Une machine ayant 3 registres courts n'est pas une excuse pour que le compilateur mélange les deux valeurs deA
etB
dans le même registre (comme indiqué dans la réponse de @ davidf), car cela briserait la sémantique du programme.Le comportement est généralement spécifié comme non défini s'il existe une raison concevable pour laquelle un compilateur qui essayait d'être "utile" pourrait faire quelque chose qui provoquerait un comportement totalement inattendu.
Dans le cas où une variable est écrite plusieurs fois sans rien garantir que les écritures se produisent à des moments distincts, certains types de matériel peuvent permettre à plusieurs opérations de «stockage» d'être exécutées simultanément à différentes adresses à l'aide d'une mémoire à double port. Cependant, certaines mémoires à double port interdisent expressément le scénario où deux magasins atteignent la même adresse simultanément, que les valeurs écrites correspondent ou non. Si un compilateur pour une telle machine remarque deux tentatives non séquencées d'écriture de la même variable, il peut refuser de compiler ou s'assurer que les deux écritures ne peuvent pas être planifiées simultanément. Mais si l'un des accès ou les deux se fait via un pointeur ou une référence, le compilateur peut ne pas toujours être en mesure de dire si les deux écritures peuvent atteindre le même emplacement de stockage. Dans ce cas, il peut planifier les écritures simultanément, provoquant une interruption matérielle lors de la tentative d'accès.
Bien sûr, le fait que quelqu'un puisse implémenter un compilateur C sur une telle plate-forme ne suggère pas qu'un tel comportement ne devrait pas être défini sur les plates-formes matérielles lors de l'utilisation de magasins de types suffisamment petits pour être traités atomiquement. Essayer de stocker deux valeurs différentes de manière non séquencée peut provoquer une bizarrerie si un compilateur n'en est pas conscient; par exemple, étant donné:
si le compilateur aligne l'appel à "moo" et peut dire qu'il ne modifie pas "v", il peut stocker un 5 à v, puis un 6 à * p, puis passer 5 à "zoo", puis passez le contenu de v au "zoo". Si "zoo" ne modifie pas "v", il ne devrait y avoir aucun moyen de transmettre des valeurs différentes aux deux appels, mais cela pourrait facilement se produire de toute façon. D'un autre côté, dans les cas où les deux magasins écriraient la même valeur, une telle bizarrerie ne pourrait pas se produire et il n'y aurait sur la plupart des plateformes aucune raison raisonnable pour qu'une implémentation fasse quelque chose de bizarre. Malheureusement, certains rédacteurs du compilateur n'ont besoin d'aucune excuse pour des comportements stupides au-delà de "parce que la norme le permet", donc même ces cas ne sont pas sûrs.
la source
Le fait que le résultat serait le même dans la plupart des implémentations dans ce cas est fortuit; l'ordre d'évaluation n'est pas encore défini. Considérez
f(i = -1, i = -2)
: ici, l'ordre est important. La seule raison pour laquelle cela n'a pas d'importance dans votre exemple est l'accident des deux valeurs-1
.Étant donné que l'expression est spécifiée comme ayant un comportement indéfini, un compilateur malicieusement conforme peut afficher une image inappropriée lorsque vous évaluez
f(i = -1, i = -1)
et abandonnez l'exécution - tout en étant considéré comme complètement correct. Heureusement, aucun compilateur à ma connaissance ne le fait.la source
Il me semble que la seule règle relative au séquencement de l'expression d'argument de fonction est ici:
Cela ne définit pas le séquencement entre les expressions d'argument, nous nous retrouvons donc dans ce cas:
Dans la pratique, sur la plupart des compilateurs, l'exemple que vous avez cité fonctionnera correctement (par opposition à "l'effacement de votre disque dur" et d'autres conséquences théoriques non définies du comportement).
Il s'agit cependant d'un passif car il dépend du comportement spécifique du compilateur, même si les deux valeurs attribuées sont identiques. De plus, évidemment, si vous tentiez d'attribuer des valeurs différentes, les résultats seraient "vraiment" indéfinis:
la source
C ++ 17 définit des règles d'évaluation plus strictes. En particulier, il séquence les arguments de fonction (bien que dans un ordre non spécifié).
Il autorise certains cas qui seraient auparavant UB:
la source
f
la signature étaitf(int a, int b)
, est-ce que C ++ 17 garantit celaa == -1
etb == -2
s'il est appelé comme dans le deuxième cas?a
etb
, alorsi
-then-a
est initialisé à -1, puisi
-then-b
est initialisé à -2, ou inversement . Dans les deux cas, on se retrouve aveca == -1
etb == -2
. C'est du moins ainsi que je lis " L'initialisation d'un paramètre, y compris chaque calcul de valeur associé et effet secondaire, est séquencée de façon indéterminée par rapport à celle de tout autre paramètre ".L'opérateur d'affectation peut être surchargé, auquel cas la commande peut être importante:
la source
Ceci répond simplement au "Je ne sais pas ce que" objet scalaire "pourrait signifier en plus de quelque chose comme un int ou un flotteur".
J'interpréterais l '"objet scalaire" comme une abréviation de "objet de type scalaire", ou simplement "variable de type scalaire". Ensuite,
pointer
,enum
(constante) sont de type scalaire.Il s'agit d'un article MSDN de types scalaires .
la source
En fait, il y a une raison pour ne pas dépendre du fait que le compilateur vérifie
i
deux fois la même valeur, de sorte qu'il est possible de la remplacer par une seule affectation. Et si nous avons des expressions?la source
1
ài
. Soit les deux arguments assignent1
et cela fait la "bonne" chose, soit les arguments assignent des valeurs différentes et c'est un comportement indéfini donc notre choix est toujours autorisé.