Qu'est-ce qui a fait i = i ++ + 1; légal en C ++ 17?

186

Avant de commencer à crier un comportement indéfini, cela est explicitement répertorié dans N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Pourtant dans N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

Qu'est ce qui a changé?

D'après ce que je peux comprendre , à partir de [N4659 basic.exec]

Sauf indication contraire, les évaluations d'opérandes d'opérateurs individuels et de sous-expressions d'expressions individuelles ne sont pas séquencées. [...] Les calculs de valeur des opérandes d'un opérateur sont séquencés avant le calcul de valeur du résultat de l'opérateur. Si un effet secondaire sur un emplacement mémoire n'est pas séquencé par rapport à un autre effet secondaire sur le même emplacement mémoire ou à un calcul de valeur utilisant la valeur de n'importe quel objet dans le même emplacement mémoire, et qu'ils ne sont pas potentiellement simultanés, le comportement n'est pas défini.

Où la valeur est définie dans [N4659 basic.type]

Pour les types trivialement copiables, la représentation de valeur est un ensemble de bits dans la représentation d'objet qui détermine une valeur , qui est un élément discret d'un ensemble de valeurs défini par l'implémentation

De [N3337 basic.exec]

Sauf indication contraire, les évaluations d'opérandes d'opérateurs individuels et de sous-expressions d'expressions individuelles ne sont pas séquencées. [...] Les calculs de valeur des opérandes d'un opérateur sont séquencés avant le calcul de valeur du résultat de l'opérateur. 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 ou à un calcul de valeur utilisant la valeur du même objet scalaire, le comportement n'est pas défini.

De même, la valeur est définie dans [N3337 basic.type]

Pour les types facilement copiables, la représentation de valeur est un ensemble de bits dans la représentation d'objet qui détermine une valeur , qui est un élément discret d'un ensemble de valeurs défini par l'implémentation.

Ils sont identiques sauf mention de la concurrence qui n'a pas d'importance, et avec l'utilisation de l' emplacement mémoire au lieu de l' objet scalaire , où

Les types arithmétiques, les types d'énumération, les types de pointeur, le pointeur vers les types de membre std::nullptr_tet les versions qualifiées cv de ces types sont collectivement appelés types scalaires.

Ce qui n'affecte pas l'exemple.

De [N4659 expr.ass]

L'opérateur d'affectation (=) et les opérateurs d'affectation composés sont tous regroupés de droite à gauche. Tous nécessitent une lvalue modifiable comme opérande gauche et renvoient une lvalue faisant référence à l'opérande gauche. Le résultat dans tous les cas est un champ de bits si l'opérande de gauche est un champ de bits. Dans tous les cas, l'affectation est séquencée après le calcul de la valeur 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.

De [N3337 expr.ass]

L'opérateur d'affectation (=) et les opérateurs d'affectation composés sont tous regroupés de droite à gauche. Tous nécessitent une lvalue modifiable comme opérande gauche et renvoient une lvalue faisant référence à l'opérande gauche. Le résultat dans tous les cas est un champ de bits si l'opérande de gauche est un champ de bits. Dans tous les cas, l'affectation est séquencée après le calcul de la valeur des opérandes droit et gauche et avant le calcul de la valeur de l'expression d'affectation.

La seule différence étant la dernière phrase étant absente de N3337.

Cependant, la dernière phrase ne devrait pas avoir d'importance car l'opérande de gauche in'est ni "un autre effet secondaire" ni "l'utilisation de la valeur du même objet scalaire" car l' expression id est une lvalue.

Passer par
la source
23
Vous avez identifié la raison pour laquelle: En C ++ 17, l'opérande droit est séquencé avant l'opérande gauche. En C ++ 11, un tel séquençage n'existait pas. Quelle est précisément votre question?
Robᵩ
4
@ Robᵩ Voir la dernière phrase.
Passer par
7
Quelqu'un a-t-il un lien avec la motivation de ce changement? Je voudrais qu'un analyseur statique soit capable de dire "vous ne voulez pas faire ça" face à du code comme i = i++ + 1;.
7
@NeilButterworth, c'est tiré de l'article p0145r3.pdf : "Refining Expression Evaluation Order for Idiomatic C ++".
xaizek
9
@NeilButterworth, la section numéro 2 dit que c'est contre-intuitif et même les experts ne parviennent pas à faire la bonne chose dans tous les cas. C'est à peu près toute leur motivation.
xaizek

Réponses:

144

En C ++ 11, l'acte d '"affectation", c'est-à-dire l'effet secondaire de la modification du LHS, est séquencé après le calcul de la valeur de l'opérande droit. Notez qu'il s'agit d'une garantie relativement "faible": elle ne produit de séquençage que par rapport au calcul de valeur du RHS. Il ne dit rien sur les effets secondaires qui pourraient être présents dans le RHS, puisque l'apparition d'effets secondaires ne fait pas partie du calcul de la valeur . Les exigences de C ++ 11 n'établissent aucun séquencement relatif entre l'acte d'assignation et les effets secondaires du RHS. C'est ce qui crée le potentiel pour UB.

Le seul espoir dans ce cas est d'éventuelles garanties supplémentaires faites par des opérateurs spécifiques utilisés dans RHS. Si le RHS utilisait un préfixe ++, les propriétés de séquençage spécifiques à la forme de préfixe ++auraient sauvé la journée dans cet exemple. Mais postfix ++est une autre histoire: il n'offre pas de telles garanties. Dans C ++ 11, les effets secondaires de =et de postfix ne sont ++pas séquencés les uns par rapport aux autres dans cet exemple. Et c'est UB.

En C ++ 17, une phrase supplémentaire est ajoutée à la spécification de l'opérateur d'affectation:

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

En combinaison avec ce qui précède, cela constitue une garantie très solide. Il séquence tout ce qui se passe dans le RHS (y compris les effets secondaires) avant tout ce qui se passe dans le LHS. Puisque l'attribution réelle est séquencée après LHS (et RHS), ce séquençage supplémentaire isole complètement l'acte d'assignation de tout effet secondaire présent dans RHS. Ce séquençage plus fort est ce qui élimine l'UB ci-dessus.

(Mis à jour pour prendre en compte les commentaires de @John Bollinger.)

Fourmi
la source
3
Est-il vraiment correct d'inclure "l'acte réel d'assignation" dans les effets couverts par "l'opérande de gauche" dans cet extrait? La norme a un langage distinct sur la séquence de l'affectation réelle. Je considère que l'extrait que vous avez présenté est limité dans sa portée au séquençage des sous-expressions gauche et droite, ce qui ne semble pas suffisant, en combinaison avec le reste de cette section, pour bien soutenir- définition de la déclaration du PO.
John Bollinger
11
Correction: l'affectation réelle est toujours séquencée après le calcul de la valeur de l'opérande gauche, et l'évaluation de l'opérande gauche est séquencée après l'évaluation (complète) de l'opérande droit, donc oui, ce changement est suffisant pour prendre en charge la définition bien définie de l'OP Interrogé sur. Je ne fais que chipoter sur les détails, mais ceux-ci importent, car ils peuvent avoir des implications différentes pour différents codes.
John Bollinger
3
@JohnBollinger: Je trouve curieux que les auteurs de la norme apportent un changement qui nuit à l'efficacité de la génération de code même simple et qui n'a pas été historiquement nécessaire, et qui rechignent pourtant à définir d'autres comportements dont l'absence est un problème beaucoup plus important, et qui poserait rarement un obstacle significatif à l’efficacité.
supercat
1
@Kaz: Pour les missions composées, d' effectuer l'évaluation de la valeur du côté gauche après le côté droit permet quelque chose comme x -= y;à traiter comme mov eax,[y] / sub [x],eaxplutôt que mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Je n'y vois rien d'idiotique. Si l'on devait spécifier un ordre, l'ordre le plus efficace serait probablement d'effectuer tous les calculs nécessaires pour identifier d' abord l'objet de gauche, puis d'évaluer l'opérande de droite, puis la valeur de l'objet de gauche, mais cela nécessiterait d'avoir un terme pour l'acte de résoudre l'identité de l'objet gauche.
supercat
1
@Kaz: Si xet ysi volatile, cela aurait des effets secondaires. En outre, les mêmes considérations s'appliqueraient à x += f();, où f()modifie x.
supercat
33

Vous avez identifié la nouvelle phrase

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

et vous avez correctement identifié que l'évaluation de l'opérande de gauche en tant que valeur l n'est pas pertinente. Cependant, séquencé avant est spécifié comme étant une relation transitive. L'opérande droit complet (y compris le post-incrément) est donc également séquencé avant l'affectation. En C ++ 11, seul le calcul de la valeur de l'opérande droit était séquencé avant l'affectation.


la source
7

Dans les anciennes normes C ++ et en C11, la définition du texte de l'opérateur d'affectation se termine par le texte:

Les évaluations des opérandes ne sont pas séquencées.

Cela signifie que les effets secondaires dans les opérandes ne sont pas séquencés et donc certainement un comportement indéfini s'ils utilisent la même variable.

Ce texte a simplement été supprimé dans C ++ 11, le laissant quelque peu ambigu. Est-ce UB ou non? Cela a été clarifié dans C ++ 17 où ils ont ajouté:

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


En remarque, dans des normes encore plus anciennes, tout cela était très clair, exemple de C99:

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.

Fondamentalement, en C11 / C ++ 11, ils se sont trompés lorsqu'ils ont supprimé ce texte.

Lundin
la source
1

Ceci est une information supplémentaire aux autres réponses et je la poste car le code ci-dessous est souvent également demandé .

L'explication dans les autres réponses est correcte et s'applique également au code suivant qui est maintenant bien défini (et ne change pas la valeur stockée de i):

i = i++;

C'est + 1un hareng rouge et on ne sait pas vraiment pourquoi le Standard l'a utilisé dans ses exemples, même si je me souviens que des gens se disputaient sur des listes de diffusion avant C ++ 11, cela a peut-être + 1fait une différence en forçant la conversion précoce de lvalue à droite- côté. Certainement rien de tout cela ne s'applique en C ++ 17 (et probablement jamais appliqué dans aucune version de C ++).

MM
la source