Il n'y a pas de cas clair de comportement indéfini ici.
Bien sûr, un argument conduisant à l'UB peut être donné, comme je l'ai indiqué dans la question, et qui a été répété dans les réponses données jusqu'à présent. Cependant, cela implique une lecture stricte de 5.17:7 qui est à la fois autocontradictoire y en contradiction avec les déclarations explicites de 5.17:1 sur l'affectation des composés. Avec une lecture plus faible de 5.17:7, les contradictions disparaissent, tout comme l'argument en faveur de l'UB. D'où ma conclusion ni l'un ni l'autre qu'il y a de l'UB ici, ni qu'il y ait un comportement clairement défini, mais les le texte de la norme n'est pas cohérent et devrait être modifié pour préciser la lecture qui prévaut (et je suppose que cela signifie qu'un rapport de défaut doit être rédigé). Bien sûr, on pourrait invoquer ici la clause de repli de la norme (la note de 1.3.24) selon laquelle les évaluations pour lesquelles la norme ne parvient pas à définir le comportement [sans ambiguïté et de manière cohérente] sont des comportements non définis, mais cela transformerait toute utilisation d'affectations composées (y compris les opérateurs d'incrémentation/décrémentation de préfixe) en UB, ce qui pourrait plaire à certains implémenteurs, mais certainement pas aux programmeurs.
Au lieu d'argumenter en faveur du problème donné, permettez-moi de présenter un exemple légèrement modifié qui fait ressortir plus clairement l'incohérence. Supposons que l'on ait défini
int& f (int& a) { return a; }
une fonction qui ne fait rien et renvoie son argument (lvalue). Modifiez maintenant l'exemple en
n += f(++n) + 1;
Notez que si la norme prévoit des conditions supplémentaires concernant le séquencement des appels de fonction, cela ne semble pas, à première vue, affecter l'exemple, puisque l'appel de fonction n'a aucun effet secondaire (pas même localement à l'intérieur de la fonction), étant donné que l'incrémentation se produit dans l'expression de l'argument de f
dont l'évaluation n'est pas soumise à ces conditions supplémentaires. En effet, appliquons l'Argument Crucial pour un Comportement Indéfini (CAUB), à savoir 5.17:7 qui dit que le comportement d'une telle affectation composée est équivalent à celui de (dans ce cas)
n = n + f(++n) + 1;
sauf que n
n'est évaluée qu'une seule fois (une exception qui ne change rien ici). L'évaluation de la déclaration que je viens d'écrire a clairement l'UB (le calcul de la valeur de la première (prvalue) n
dans le RHS n'est pas séquencée par rapport à l'effet secondaire du ++
qui implique le même objet scalaire (1.9:15) et vous êtes mort).
Ainsi, l'évaluation des n += f(++n) + 1
a un comportement indéfini, n'est-ce pas ? C'est faux ! Lisez dans 5.17:1 que
En ce qui concerne un appel de fonction à séquence indéterminée, l'opération d'une affectation composée est une évaluation unique. [ Note : Par conséquent, un appel de fonction ne doit pas intervenir entre la conversion de lvalue en rvalue et l'effet secondaire associé à un seul opérateur d'affectation composé. - note de fin ]
Cette formulation est loin d'être aussi précise que je le souhaiterais, mais je ne pense pas qu'il soit exagéré de supposer que "à séquence indéterminée" devrait signifier "en ce qui concerne cette opération d'une affectation composée". La note (non normative, je sais) indique clairement que la conversion de lvalue en rvalue fait partie de l'opération de l'affectation composée. L'appel de f
à séquence indéterminée en ce qui concerne le fonctionnement de l'affectation composée de +=
? Je ne suis pas sûr, car la relation "séquencée" est définie pour les calculs de valeurs individuelles et les effets secondaires, et non pour les évaluations complètes d'opérateurs, qui peuvent impliquer les deux. En fait, l'évaluation d'un opérateur d'affectation composé implique trois éléments : la conversion de lvalue en rvalue de son opérande gauche, l'effet de bord (l'affectation proprement dite) et le calcul de la valeur de l'affectation composée (qui est séquencée après l'effet de bord et renvoie l'opérande gauche d'origine en tant que lvalue). Notez que l'existence de la conversion de lvalue en rvalue n'est jamais explicitement mentionnée dans la norme sauf dans la note citée ci-dessus En particulier, la norme ne fait aucune (autre) déclaration concernant son ordre par rapport à d'autres évaluations. Il est assez clair que dans l'exemple, l'appel de f
est séquencée avant l'effet secondaire et le calcul de la valeur de +=
(puisque l'appel se produit dans le calcul de la valeur de l'opérande de droite à +=
), mais il se peut que la séquence soit indéterminée en ce qui concerne la partie de la conversion de lvalue en rvalue. Je me souviens, d'après ma question, que puisque l'opérande gauche de +=
est une valeur l (et c'est nécessairement le cas), on ne peut pas considérer que la conversion de valeur l en valeur r s'est produite dans le cadre du calcul de la valeur de l'opérande de gauche.
Toutefois, en vertu du principe du milieu exclu, l'appel au f
doit être soit en séquence indéterminée par rapport à l'opération de l'affectation composée de +=
ou non séquencée de manière indéterminée par rapport à elle ; dans ce dernier cas, elle doit être séquencée avant parce qu'il n'est pas possible qu'il soit séquencé après lui (l'appel de f
de séquençage avant que l'effet secondaire de la +=
et la relation étant antisymétrique). Supposons donc d'abord que est à séquence indéterminée par rapport à l'opération. Ensuite, la clause citée dit qu'en ce qui concerne l'appel de f
l'évaluation des +=
est une opération unique, et la note explique que cela signifie que l'appel ne doit pas intervenir entre la conversion de lvalue en rvalue et l'effet de bord associé à +=
il devrait être placé soit avant les deux, soit après les deux. Mais il n'est pas possible d'être séquencé après l'effet secondaire, il doit donc être placé avant les deux. Cela rend (par transitivité) l'effet secondaire de ++
séquencée avant la conversion lvalue-to-rvalue, exit UB. Supposons ensuite que l'appel de f
est séquencée avant l'opération de +=
. Il est donc en particulier séquencé avant la conversion de lvalue en rvalue, et toujours par transitivité, il en est de même pour l'effet secondaire de ++
; pas d'UB dans cette branche non plus.
Conclusion : 5.17:1 contredit 5.17:7 si ce dernier est considéré (CAUB) comme normatif pour les questions d'UB résultant d'évaluations non séquencées par 1.9:15. Comme je l'ai dit, CAUB est également auto-contradictoire (par les arguments indiqués dans la question), mais cette réponse devient trop longue, je vais donc en rester là pour l'instant.
Trois problèmes et deux propositions pour les résoudre
En essayant de comprendre ce que la norme écrit sur ces questions, je distingue trois aspects dans lesquels le texte est difficile à interpréter ; ils sont tous de nature à ce que le texte ne soit pas suffisamment clair sur le modèle auquel ses déclarations se réfèrent. (Je cite les textes à la fin des éléments numérotés, car je ne connais pas le balisage permettant de reprendre un élément numéroté après une citation)
-
Le texte de 5.17:7 est d'une simplicité apparente qui, bien que l'intention soit facile à saisir, nous donne peu de prise lorsqu'il est appliqué à des situations difficiles. Il fait une affirmation générale (comportement équivalent, apparemment à tous égards) mais dont l'application est contrariée par la clause d'exception. Et si le comportement de E1 = E1
op E2
est indéfini ? Dans ce cas, il s'agit de E1
op = E2
devrait l'être également. Mais que se passerait-il si l'UB était en raison de E1
évaluée deux fois en E1 = E1
op E2
? En évaluant ensuite E1
op = E2
ne devrait vraisemblablement pas être l'UB, mais si c'est le cas, alors défini comme quoi ? C'est comme dire "la jeunesse du second jumeau a été exactement comme celle du premier, sauf qu'il n'est pas mort à l'accouchement". Franchement, je pense que ce texte, qui a peu évolué depuis la version C "A Affectation d'un composé de la forme E1 op = E2
diffère de la simple expression d'affectation E1 = E1 op E2
uniquement dans la mesure où la valeur l E1
n'est évalué qu'une seule fois" pourrait être adapté pour correspondre aux changements de la norme.
(5.17) 7 Le comportement d'une expression de la forme E1
op = E2
est équivalent à E1 = E1
op E2
sauf que E1
n'est évaluée qu'une seule fois [...]
-
La définition précise des actions (évaluations) entre lesquelles la relation "séquentielle" est définie n'est pas très claire. Il est dit (1.9:12) que l'évaluation d'une expression comprend et l'apparition d'effets secondaires. Bien que cela semble indiquer qu'une évaluation peut avoir plusieurs composants (atomiques), la relation séquentielle est en fait principalement définie (par exemple en 1.9:14,15) pour des composants individuels, de sorte qu'il serait préférable de lire cela comme étant que la relation séquentielle est définie comme étant la relation entre les composants atomiques et la relation séquentielle. notion Le terme "évaluation" englobe à la fois les calculs de valeur et les effets secondaires (initiés). Cependant, dans certains cas, la relation "séquentielle" est définie pour l'exécution (entière) d'une expression ou d'une déclaration (1.9:15) ou pour un appel de fonction (5.17:1), même si un passage de 1.9:15 évite ce dernier cas en se référant directement aux exécutions dans le corps d'une fonction appelée.
(1.9) 12 L'évaluation d'une expression (ou d'une sous-expression) comprend en général à la fois des calculs de valeurs (...) et le déclenchement d'effets de bord. [...] 13 Séquencé avant est une relation asymétrique, transitive et par paire entre les évaluations exécutées par un seul thread [...] 14 Chaque calcul de valeur et effet de bord associé à une expression complète est séquencé avant chaque calcul de valeur et effet de bord associé à l'expression complète suivante à évaluer. [...] 15 Lors de l'appel d'une fonction (que la fonction soit en ligne ou non), chaque calcul de valeur et chaque effet de bord associé à un argument ou à une expression est évalué avant chaque calcul de valeur et chaque effet de bord associé à l'expression suivante. associés à toute expression d'argument ou à l'expression postfixe désignant la fonction appelée sont séquencés avant l'exécution de chaque expression. est séquencée avant l'exécution de chaque expression ou instruction dans le corps de la fonction appelée. [...] Chaque évaluation dans la fonction appelante (y compris les autres appels de fonction) [...] est séquencée de manière indéterminée par rapport à l'exécution de l'expression ou de l'instruction dans le corps de la fonction appelée. par rapport à l'exécution de la fonction appelée [...] (5.2.6, 5.17) 1 ... En ce qui concerne un appel de fonction à séquence indéterminée, ...
-
Le texte devrait reconnaître plus clairement qu'une affectation composée implique, contrairement à une affectation simple, l'action de récupérer la valeur précédemment affectée à son opérande de gauche ; cette action est semblable à la conversion de lvaleur en rvaleur, mais ne se produit pas dans le cadre du calcul de la valeur de cet opérande de gauche, puisqu'il ne s'agit pas d'une prvaleur ; en fait, c'est un problème que 1.9:12 ne reconnaisse qu'une telle action. pour l'évaluation de la valeur pr . En particulier, le texte devrait être plus clair quant aux relations "séquencées" données pour cette action, le cas échéant.
(1.9) 12 L'évaluation d'une expression... comprend... des calculs de valeur (y compris la détermination de l'identité d'un objet pour l'évaluation glvalue et la récupération d'une valeur précédemment attribuée à un objet pour l'évaluation prvalue)
Le deuxième point est le moins directement lié à notre question concrète, et je pense qu'il peut être résolu simplement en choisissant un point de vue clair et en reformulant les passages qui semblent indiquer un point de vue différent. Etant donné que l'un des principaux objectifs des anciens points de séquence, et maintenant de la relation "séquencée", était de préciser que l'effet secondaire des opérateurs postfixe-incrément n'est pas séquencé par rapport aux actions séquencées après le calcul de la valeur de cet opérateur (ce qui donne par ex. i = i++
UB), le point de vue doit être que individuel les calculs de valeur et (le déclenchement) des effets secondaires individuels sont des "évaluations" pour lesquelles on peut définir une "séquence avant". Pour des raisons pragmatiques, j'inclurais également deux autres types d'"évaluations" (triviales) : l'entrée dans une fonction (de sorte que le langage de 1.9:15 puisse être simplifié comme suit : "Lors de l'appel d'une fonction...") : "Lors de l'appel d'une fonction..., chaque calcul de valeur et effet de bord associé à l'une de ses expressions d'argument, ou à l'expression postfixe désignant la fonction appelée, est séquencé avant l'entrée de cette fonction") et la sortie de fonction (de sorte que toute action dans le corps de la fonction soit, par transitivité, séquencée avant tout ce qui requiert la valeur de la fonction ; ceci était garanti par un point de séquence, mais la norme C++11 semble avoir perdu cette garantie ; ceci pourrait rendre l'appel d'une fonction se terminant par return i++;
potentiellement UB là où ce n'est pas prévu, et utilisé pour être sûr). Ensuite, on peut également être clair sur la relation "séquentielle indéterminée" des appels de fonctions : pour chaque appel de fonction et chaque évaluation qui ne fait pas partie (directement ou indirectement) de l'évaluation de cet appel, cette évaluation doit être séquencée (avant ou après) par rapport à l'entrée et à la sortie de cet appel de fonction, et il a la même relation dans les deux cas (de sorte qu'en particulier, ces actions externes ne peuvent pas être séquencées après l'entrée de la fonction mais avant la sortie de la fonction, comme cela est clairement souhaitable au sein d'un seul thread).
Pour résoudre les points 1 et 3, je vois deux voies (chacune affectant les deux points), qui ont des conséquences différentes sur le comportement défini ou non de notre exemple :
Affectations composées avec deux opérandes et trois évaluations
Les opérations composées ont leurs deux opérandes habituels, un opérande gauche lvalue et un opérande droit prvalue. Pour remédier au manque de clarté de l'article 3, il est précisé dans le paragraphe 1.9:12 que la récupération de la valeur précédemment attribuée à un objet peut également avoir lieu dans le cadre d'affectations composées (et non pas uniquement pour l'évaluation d'une prvaleur). La sémantique des affectations composées est définie en modifiant 5.17:7 en
Dans une affectation composée op =
la valeur précédemment assignée à l'objet auquel se réfère l'opérande de gauche est récupérée, l'opérateur op est appliqué avec cette valeur comme opérande gauche et l'opérande droit de op =
comme opérande de droite, et la valeur résultante remplace celle de l'objet auquel se réfère l'opérande de gauche.
(Cela donne deux évaluations, la recherche et l'effet secondaire ; une troisième évaluation est le calcul de la valeur triviale de l'opérateur composé, séquencée après les deux autres évaluations).
Pour plus de clarté, indiquez clairement dans 1.9:15 que les calculs de valeur dans les opérandes sont séquencés avant tous les calculs de valeur associés à l'opérateur (plutôt que les seuls calculs de valeur associés à l'opérateur). pour le résultat de l'opérateur ), ce qui garantit que l'évaluation de l'opérande gauche de valeur l est séquencée avant de récupérer sa valeur (on peut difficilement imaginer autre chose), et séquence également le calcul de la valeur de l'opérande de valeur l. droit avant cette recherche, ce qui exclut UB dans notre exemple. Tant qu'à faire, je ne vois aucune raison de ne pas séquencer également les calculs de valeur dans les opérandes avant tout les effets secondaires associées à l'opérateur (comme elles doivent l'être), ce qui rendrait superflue la mention explicite de ce point pour les affectations (composées) dans la section 5.17:1. D'un autre côté, il est mentionné que la recherche de valeur dans une affectation composée est séquencée avant son effet de bord.
Affectations composées avec trois opérandes et deux évaluations
Afin d'obtenir que la recherche dans une affectation de compte ne soit pas séquencée par rapport au calcul de la valeur de l'opérande de droite, ce qui rendrait notre exemple UB, la manière la plus claire semble être de donner aux opérateurs composés un troisième (milieu) implicite. opérande , une valeur pr, non représentée par une expression distincte, mais obtenue par conversion de valeur l en valeur r à partir de l'opérande gauche (cette nature à trois opérandes correspond à la forme étendue des affectations composées, mais en obtenant l'opérande du milieu à partir de l'opérande gauche, on s'assure que la valeur est extraite du même objet que celui dans lequel le résultat sera stocké, une garantie cruciale qui n'est que vaguement et implicitement donnée dans la formulation actuelle par l'expression "sauf que"). E1
n'est évaluée qu'une seule fois"). La différence avec la solution précédente est que la recherche est maintenant une véritable conversion de lvaleur à rvaleur (puisque l'opérande du milieu est une prvaleur) et qu'elle est effectuée dans le cadre du calcul de la valeur des opérandes de l'affectation composée ce qui le rend naturellement non séquencé avec le calcul de la valeur de l'opérande de droite. Il faudrait préciser quelque part (dans une nouvelle clause décrivant cet opérande implicite) que le calcul de la valeur de l'opérande gauche est séquencé avant cette conversion de lvaleur en rvaleur (il est clair qu'il doit l'être). Maintenant, 1.9:12 peut être laissé tel quel, et à la place de 5.17:7 je propose
Dans une affectation composée op =
avec l'opérande gauche a
(une valeur l), et les opérandes moyen et droit b
respectivement c
(les deux valeurs), l'opérateur op est appliquée avec b
comme opérande gauche et c
comme opérande de droite, et la valeur résultante remplace celle de l'objet référencé par a
.
(Cela donne une évaluation, l'effet secondaire, avec comme seconde évaluation le calcul de la valeur triviale de l'opérateur composé, séquencé après lui).
Les modifications de 1.9:15 et de 5.17:1 suggérées dans la solution précédente pourraient toujours s'appliquer, mais ne donneraient pas à notre exemple original un comportement défini. Cependant, l'exemple modifié au début de cette réponse aurait toujours un comportement défini, à moins que la partie 5.17:1 "l'affectation composée est une opération unique" ne soit supprimée ou modifiée (il y a un passage similaire dans 5.2.6 pour l'incrémentation/décrémentation du postfixe). L'existence de ces passages suggérerait que le détachement des opérations fecth et store au sein d'une assignation composée unique ou d'une incrémentation/décrémentation de postfixe était no l'intention des rédacteurs de la norme actuelle (et, par extension, de notre exemple d'UB), mais il ne s'agit bien sûr que d'une simple supposition.