Pourquoi est f(i = -1, i = -1) un comportement indéfini?
Que j'ai lu sur l'ordre d'évaluation des violations, et ils donnent un exemple qui m'intrigue, c'.
1) Si un effet indésirable sur un scalaire objet est non-séquencés par rapport à un autre effet secondaire sur la même scalaire objet, le comportement est indéfini.
//snip f(i = -1, i = -1); //undefined behavior
Dans ce contexte, i
est un scalaire objet, ce qui signifie apparemment
Arithmétique types (3.9.1), l'énumération des types, des types pointeur, pointeur de types de membres (3.9.2), std::nullptr_t, et de cv qualifiés versions de ces types (3.9.3) sont collectivement appelés types scalaires.
Je ne vois pas comment l'énoncé 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 -1
, et les deux arguments sont également -1
.
Quelqu'un peut-il préciser?
Mise à JOUR
J'ai vraiment apprécier toutes les discussions. Jusqu'à présent, j'aime @harmaline réponse beaucoup car il expose les pièges et subtilités de la définition de cette déclaration, en dépit de la façon dont tout droit vers l'avant, il semble au premier coup d'œil. @acheong87 souligne certains problèmes qui surgissent lors de l'utilisation de références, mais je pense que c'est orthogonale à la non effets secondaires aspect de cette question.
RÉSUMÉ
Depuis que cette question a obtenu une tonne de l'attention, je vais résumer les principaux points et/ou des réponses. Tout d'abord, permettez-moi une petite digression à souligner que "pourquoi" peut avoir étroitement liés et pourtant subtilement différentes significations, à savoir "ce que cause", "pour quoi raison", et "pour quoi but". Je vais regrouper les réponses par lequel de ces sens de "pourquoi" ils se sont adressés.
pour quelle cause
La principale réponse ici provient de Paul Draper, avec Martin J contribuant similaire, mais pas aussi étendues de réponse. Paul Draper la réponse se résume à
C'est un comportement indéfini, car il n'est pas défini ce que le comportement est.
La réponse est dans l'ensemble très bon en termes d'expliquer ce que le C++ standard dit. Il aborde également certains liés à des cas d'UB comme f(++i, ++i);
et f(i=1, i=-1);
. Dans le premier des cas, il n'est pas clair si le premier argument doit être i+1
et la deuxième i+2
ou vice-versa; dans le second, il n'est pas clair si i
devrait être de 1 ou de -1, après l'appel de la fonction. Deux de ces cas sont des cas d'UB, parce qu'ils relèvent de la règle suivante:
Si un effet indésirable sur un scalaire objet est séquencé par rapport à un autre effet secondaire sur la même scalaire objet, le comportement est indéfini.
Par conséquent, f(i=-1, i=-1)
est également UB car il tombe sous la même règle, malgré que l'intention du programmeur est (à mon humble avis) c'est évident et sans ambiguïté.
Paul Draper rend également explicite dans sa conclusion que
Aurait-il été défini comportement? Oui. Était-il défini? No.
qui nous amène à la question "pour quelles raisons a été f(i=-1, i=-1)
gauche comme un comportement indéfini?"
pour quelles raisons
Bien qu'il existe quelques oublis (peut-être imprudente) dans la norme C++, de nombreuses omissions sont bien motivés et servir un but précis. Même si je suis conscient que le but est souvent de "faire le compilateur-écrivain travail plus facile", ou "plus vite code", j'ai surtout été intéressé de savoir si il y a une bonne raison de quitter f(i=-1, i=-1)
comme UB.
harmaline et supercat fournir les principales réponses qui fournissent une raison pour l'UB. Harmaline, un compilateur optimisant qui pourraient briser soi-disant atomique des opérations d'affectation en plusieurs instructions machine, et qu'il pourrait en outre interleave ces instructions pour une vitesse optimale. Cela pourrait conduire à des résultats surprenants: i
finit -2 dans son scénario! Ainsi, harmaline montre comment affecter le même valeur à une variable plus d'une fois, peut avoir des effets pervers si les opérations sont séquencé.
supercat fournit un liés à l'exposition des pièges d'essayer d'obtenir f(i=-1, i=-1)
de faire ce qu'il dirait qu'il devrait faire. Il souligne que sur certaines architectures, il y a de dur restrictions à l'encontre des multiples et simultanées écrit à la même adresse mémoire. Un compilateur pourrait avoir un moment difficile de l'attraper si on avait affaire à quelque chose de moins banal que f(i=-1, i=-1)
.
davidf fournit également un exemple d'entrelacement des instructions très similaire à l'harmaline de l'.
Bien que chacun de harmaline, de supercat et davidf' exemples sont un peu artificiel, prises ensemble, elles servent encore de fournir une réelle raison pourquoi f(i=-1, i=-1)
devrait être un comportement indéfini.
J'ai accepté harmaline de la réponse, car elle fait le meilleur travail de traiter l'ensemble des significations de pourquoi, même si Paul Draper de la réponse adressée le "pourquoi" de la portion de mieux.
d'autres réponses
JohnB souligne que, si l'on considère surcharge des opérateurs d'affectation (au lieu de simplement les scalaires), alors on peut avoir des problèmes ainsi.
- Un scalaire objet est un objet de type scalaire. Voir 3.9/9: "de l'Arithmétique types (3.9.1), l'énumération des types, des types pointeur, pointeur de types de membres (3.9.2),
std::nullptr_t
, et de cv qualifiés versions de ces types (3.9.3) sont collectivement appelés types scalaires." - Peut-être il y a une erreur sur la page, et il s'agit
f(i-1, i = -1)
ou quelque chose de similaire. - Jetez un oeil à cette question: stackoverflow.com/a/4177063/71074
- Merci. N' "arithmétique" types comprennent bool?
- Eh bien, je suppose que la réponse est simple: l'ordre d'évaluation des paramètres dépend de la convention d'appel utilisé par le compilateur/architecture - qui le programmeur n'a pas beaucoup de contrôle(?!) et donc, le comportement peut être différent d'un compilateur/architecture à l'autre.
- SchighSchagh votre mise à jour devrait être dans la section de réponse.
- Vous devriez probablement mentionner dans le résumé, que, depuis le C++17 ce n'est pas UB plus, voir @AlexDs réponse
Vous devez vous connecter pour publier un commentaire.
Puisque les opérations sont non, il n'y a rien de dire que les instructions de réaliser la mission ne peut pas être entrelacées. Il peut être optimal de le faire, en fonction de l'architecture du PROCESSEUR. Référencé dans la page indique ceci:
Que par lui-même ne semble pas comme il serait la cause d'un problème - en supposant que l'opération en cours est de stocker la valeur -1 dans un emplacement de mémoire. Mais il n'y a rien de dire que le compilateur ne peut pas optimiser que dans un ensemble d'instructions qui a le même effet, mais qui pourrait échouer si l'opération a été intercalées avec une autre opération sur le même emplacement de mémoire.
Par exemple, imaginez que c'était plus efficace à zéro de la mémoire, puis de décrémentation, par rapport à charger la valeur -1 dans. Puis ceci:
pourrait devenir:
Maintenant i est -2.
Il est probablement un faux exemple, mais c'est possible.
load 8bit immediate and shift
jusqu'à 4 fois. Généralement, le compilateur ne adressage indirect pour extraire un numéro de la forme d'une table pour éviter cela. (-1 peut être fait en 1 instruction, mais un autre exemple pourrait être choisi).unsigned char foo=255
; peut-êtremovlw 255 / movwf _foo
ouclrf _foo / decf foo
. L'ancien testament de la charge de travail s'inscrire avec 255; ce dernier permettra de le laisser tranquille.-Wunsequenced
(je présume que GCC serait de faire ainsi).Première, "scalaire objet" désigne un type comme un
int
,float
, ou un pointeur (voir Ce qui est un scalaire Objet en C++?).Seconde, il peut sembler plus évident que
aurait un comportement indéfini. Mais
est moins évident.
Un exemple légèrement différent:
Attribution de ce qui s'est passé "dernier",
i = 1
, oui = -1
? Ce n'est pas définie dans la norme. Vraiment, cela signifie quei
pourrait être5
(voir l'harmaline, en réponse à une explication plausible pour l'devriez être le cas). Ou vous programme pourrait erreur de segmentation. Ou de reformater votre disque dur.Mais maintenant, vous demandez: "Quel sujet de mon exemple? J'ai utilisé la même valeur (
-1
) pour les deux missions. Ce qui pourrait éventuellement être difficile de savoir à ce sujet?"Vous avez raison...sauf dans la façon dont le C++ comité de normalisation décrit cette.
Ils pourrait ont fait une exception pour votre cas particulier, mais ils n'ont pas. (Et pourquoi devraient-ils? Quelle sera l'utilisation que peut en avoir?) Donc,
i
pourrait encore être5
. Ou de votre disque dur pourrait être vide. Donc la réponse à votre question est:C'est un comportement indéfini, car il n'est pas défini ce que le comportement est.
(Ce qui mérite d'être soulignée, car de nombreux programmeurs pensent "undefined" signifie "au hasard", ou "imprévisible". Il n'en est rien; il ne signifie pas définie par la norme. Le comportement pourrait être 100% compatible, et encore non défini.)
Aurait-il été défini comportement? Oui. Était-il défini? Pas de. Par conséquent, il est "undefined".
Qui a dit, "undefined" ne veut pas dire qu'un compilateur de formater votre disque dur...cela veut dire qu'il pourrait et il serait toujours conforme aux normes du compilateur. De façon réaliste, je suis sûr que g++, Clang, et MSVC tous faire ce qui vous attend. Ils n'ont pas de".
Une autre question qui est peut être Pourquoi le C++ comité des normes de choisir pour faire de ce côté-effet non?. Cette réponse impliquera de l'histoire et de l'avis de la commission. Ou Ce qui est bon au sujet de avoir de ce côté-effet non en C++?, ce qui permet à toute justification, si oui ou non il était le raisonnement de la commission des normes. Vous pourriez poser ces questions ici, ou à programmers.stackexchange.com.
-Wsequence-point
g++, il vous en avertira.-fsanitize=undefined
qui, même si elle peut ne pas attraper ce dans sa forme actuelle (je ne suis pas sûr), pourrait dans l'avenir.harmic
a fourni un excellent exemple de POURQUOI cette UB peut générer des effets pervers. Les compilateurs modernes vraiment parfois faire de telles optimisations. Pas sur votre CPU? Déplacer vers Itanium. Ou AVR. Ou, Atari. (laissez-moi simplement ajouter que j'ai ceux de la plateforme sont des exemples de rhétorique et qui ne sont pas vérifiées 😉 )i
pourrait être de 5." dans le contexte du code en cours de discussion? Je suis d'accord que, compte tenu def(i = 1, i = -1)
la variablei
pourrait être 1 ou -1, mais je ne vois pas commenti
pourrait être affecté d'une valeur de 5 donné le code comme indiqué.i
sont entrelacés; ce n'est pas raisonnable de comportement de la part du compilateur. Il pourrait générerclear; decr; clear; decr
, maisclear; clear; decr; decr
ne serait pas le résultat de quelque chose que l'exige le code, et la seule chose généré doit être un rapport de bogue.It is undefined behavior because it is not defined what the behavior is.
Pour moi, c'est tautologique réponse est plutôt insatisfaisant. Personnellement, j'ai tendance à associer UB avec des choses qui, naturellement, ne faites pas de sens ou qui est autrement difficile de définir. J'apprécie la discussion ici, mais je préfère l'harmaline de réponse qui permet de mieux exposer les pièges d'essayer de définir ce. (Votre réponse presque indices que la spécification du comité a été trop paresseux.)undefined behavior
signifiesomething random will happen
, ce qui est loin d'être le cas la plupart du temps.i
finit-1
", c'est qu'il n'est pas garanti 😉It is undefined behavior because it is not defined what the behavior is.
.x == x
est correct. Correct ne veut pas dire utile. Si j'ai demandé à Pourquoi X n'est pas le cas?, quelqu'un pouvait répondre à X n'est pas le cas, car il n'est pas le cas que X., et ils seraient de la même façon exact. Mais ces tautologies sont en aucun cas utile. En tout cas, votre récente clarification a supprimé la tautologie de la tautologie, pour ainsi dire. Avoir un upvote.Une raison pratique pour ne pas faire une exception aux règles simplement parce que les deux valeurs sont les mêmes:
Considérer le cas cela était autorisé.
Maintenant, quelques mois plus tard, le besoin de changer
Apparemment inoffensif, n'est-ce pas? Et pourtant, soudain, prog.cpp ne compile pas plus.
Pourtant, nous pensons que la compilation ne doit pas dépendre de la valeur d'un littéral.
Bottom line: il n'y a pas d'exception à la règle parce qu'il ferait compilation réussie dépend de la valeur (plutôt le type) d'une constante.
MODIFIER
@Dispositif heartware souligné qu'une des expressions de la forme
A DIV B
ne sont pas autorisés dans certaines langues, quandB
est 0, et la cause de la compilation à l'échec. C'est pourquoi le changement d'une constante pourrait provoquer des erreurs de compilation dans un autre lieu. Qui est, à mon humble avis, regrettable. Mais il est certainement une bonne chose pour limiter de telles choses à l'inévitable.f(i = VALUEA, i = VALUEB);
a certainement le potentiel pour un comportement indéterminé. J'espère que vous n'êtes pas vraiment de codage par rapport à des valeurs de derrière les identificateurs.SomeProcedure(A, B, B DIV (2-A))
. De toute façon, si les états langue que CONST doivent être évaluées au moment de la compilation, alors, bien sûr, ma demande n'est pas valide pour ce cas. Puisque de toute façon il brouille la distinction de compile-time et de l'exécution. Serait-il également d'avis que si nous écrireCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
?? Ou sont les fonctions qui ne sont pas autorisés?Yet, we feel that compilation should not depend on the value of a literal.
Meh. Vous pouvez faire beaucoup de trucs cool avec des littéraux, en particulier dans le modèle de méta-programmation, mais évidemment pas tous les possibles littérale sera toujours judicieux. Aussi, je pense que votre réponse manque le point un peu. Mais je vous remercie pour la discussion de toute façon.f(i = 1, i = 2)
est à juste titre pas défini, et tente de donner une pratique (plutôt que de la technique) pourquoi pas pour faire une exception à la règle dans le cas particulierf(i = 1, i = 1)
(comme quelqu'un ici a suggéré).La confusion est que le stockage d'une valeur constante à une variable locale n'est pas une instruction atomique sur chaque architecture, la C est conçu pour être exécuté sur. Le processeur le code s'exécute sur des sujets plus que le compilateur dans ce cas. Par exemple, sur les BRAS où chaque instruction ne peut pas porter un complet de 32 bits constant, stocker un int dans une variable a besoin de plus qu'une instruction. Exemple avec cette pseudo-code où vous pouvez stocker uniquement les 8 bits à la fois et doit travailler dans un 32 bits de registre, je est un int32:
Vous pouvez imaginer que si le compilateur veut optimiser l'on peut intercaler d'une même séquence deux fois, et vous ne savez pas quelle valeur sont écrites dans i; et disons qu'il n'est pas très intelligent:
Cependant, dans mes tests, gcc est assez aimable pour reconnaître que la même valeur est utilisée deux fois et génère une seule fois, et n'a rien de bizarre. Je reçois -1, -1
Mais mon exemple est encore valide, il est important de considérer que même une constante ne peut pas être aussi évidente qu'elle semble être.
-1
(que le compilateur a stocké quelque part), mais c'est plutôt3^81 mod 2^32
, mais constante, alors le compilateur peut faire exactement ce qui est fait ici, et dans certains levier de omtimization, mon interleave l'appel des séquences pour éviter l'attente.f(i = A, j = B)
oùi
etj
sont deux objets distincts. Cet exemple n'a pas d'UB. Machine ayant 3 court registres n'est pas une excuse pour le compilateur de mélanger les deux valeurs deA
etB
dans le même registre (comme indiqué dans @davidf de réponse), parce qu'il allait se casser la sémantique du programme.Comportement est généralement spécifié non définies s'il est concevable de raison d'un compilateur qui essayait d'être "utile" peut faire quelque chose qui serait la cause totalement inattendue.
Dans le cas où une variable est écrit plusieurs fois avec rien pour s'assurer que les écritures se produire à des moments distincts, certains types de matériel peut autoriser plusieurs "stocker" les opérations qui doivent être effectuées simultanément à différentes adresses à l'aide d'un double port mémoire. Cependant, certaines à double port souvenirs interdisent expressément le scénario où deux magasins frappé de la même adresse simultanément, indépendamment de si oui ou non les valeurs écrites match. Si un compilateur pour une telle machine remarque deux séquencé tente d'écrire la même variable, il peut soit refuser de compiler ou de s'assurer que les deux écritures ne peuvent pas obtenir prévus en même temps. Mais si l'un ou les deux de l'accès est par l'intermédiaire d'un pointeur ou d'une référence, le compilateur ne peut pas toujours être en mesure de dire si les deux écritures peuvent frapper le même emplacement de stockage. Dans ce cas, il peut planifier l'écrit simultanément, provoquant un matériel de piège sur la tentative d'accès.
Bien sûr, le fait que quelqu'un pourrait mettre en œuvre un compilateur C sur une telle plate-forme ne permet pas de croire qu'un tel comportement ne devrait pas être définis sur des plates-formes matérielles lors de l'utilisation de magasins de types assez petit pour être traitées de manière atomique. Essayez de stocker deux valeurs différentes dans séquencé de la mode pourrait causer étrangeté, si un compilateur n'est pas au courant de celui-ci; par exemple, étant donné:
si le compilateur dans les lignes de l'appel à "meuh" et vous pouvez dire qu'il ne modifie pas la
"v", il peut stocker un 5 à v, puis stocker un 6 *p, puis passer 5 à "zoo",
et puis passer le contenu de v pour "zoo". Si "zoo" ne pas modifier "v",
il devrait y avoir aucun moyen de les deux appels doivent être transmises des valeurs différentes,
mais qui pourrait facilement passer de toute façon. D'autre part, dans les cas où
deux magasins à écrire la même valeur, telle étrangeté ne pourrait pas se produire et
il y aurait sur la plupart des plateformes être pas de raison logique pour une mise en œuvre
à faire des choses bizarres. Malheureusement, certains rédacteurs de compilateur n'en a pas besoin
excuse stupide comportements au-delà de "parce que la Norme le permet", de sorte que même
ces cas ne sont pas sécuritaires.
Le fait que le résultat serait le même dans la plupart des implémentations dans ce cas, est accessoire; l'ordre d'évaluation n'est toujours pas défini. Envisager
f(i = -1, i = -2)
: ici, l'ordre des questions. La seule raison pour laquelle il n'a pas d'importance dans votre exemple, l'accident que les deux valeurs sont-1
.Étant donné que l'expression est spécifié comme l'une avec un comportement indéfini, un malicieusement conforme compilateur peut afficher une image inappropriée lorsque vous évaluez
f(i = -1, i = -1)
et interrompt l'exécution - et toujours être considéré comme tout à fait correct. Heureusement, pas de compilateurs je suis conscient de le faire.Il me semble que la seule règle concernant le séquençage de l'argument de fonction de l'expression est ici:
Ce n'est pas de définir le séquençage entre les expressions d'arguments, on se retrouve donc dans ce cas:
Dans la pratique, sur la plupart des compilateurs, l'exemple que vous avez cité fonctionnent très bien (par opposition à "l'effacement du disque dur" et d'autres théorique comportement indéfini conséquences).
Il est, cependant, une responsabilité, puisque cela dépend du compilateur spécifique de comportement, même si les deux valeurs sont les mêmes. Aussi, évidemment, si vous avez essayé d'attribuer des valeurs différentes, le résultat serait "vraiment" undefined:
C++17 définit plus strictes règles d'évaluation. En particulier, les séquences des arguments de la fonction (bien que dans un ordre non spécifié).
Il permet à certains cas qui serait UB avant:
f
's signature ont étéf(int a, int b)
, est-ce que C++17 garantir quea == -1
etb == -2
si on l'appelle comme dans le deuxième cas?a
etb
, puisi
-puis-a
sont initialisé à -1, aprèsi
-puis-b
sont initialisées à -2, ou le moyen de la contourner. Dans les deux cas, nous nous retrouvons aveca == -1
etb == -2
. Au moins c'est comment j'ai lu "L'initialisation d'un paramètre, y compris tous les associés de la valeur de calcul et d'effets secondaires, est pour une période indéterminée séquencé par rapport à celle de tout autre paramètre".De l'opérateur d'affectation pourrait être surchargé, qui pourrait avoir son importance:
C'est juste pour répondre à "je ne suis pas sûr de ce que "scalaire objet" pourrait se traduire en plus de quelque chose comme un int ou un float".
Je interpréter le "scalaire objet" comme une abréviation de "type scalaire objet", ou simplement "variable de type scalaire". Ensuite,
pointer
,enum
(constante) sont de type scalaire.C'est un article de MSDN de Types Scalaires.
En fait, il y a une raison de ne pas dépendre du fait que le compilateur vérifiera que
i
est attribué, avec la même valeur à deux reprises, de sorte qu'il est possible de le remplacer avec de l'affectation unique. Que faire si nous avons certaines expressions?1
ài
. Soit les deux arguments attribuer1
et ce n'est la "bonne" chose, ou les arguments attribuer des valeurs différentes et c'est un comportement indéfini de sorte que notre choix est encore possible.