195 votes

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

Avant que vous ne commenciez à crier au comportement indéfini, c'est explicitement répertorié dans N4659 (C++17)

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

Pourtant, en N3337 (C++11)

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

Qu'est-ce qui a changé ?

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

Sauf indication contraire, les évaluations des opérandes des opérateurs individuels et des sous-expressions des 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 de mémoire n'est pas séquencé par rapport à un autre effet secondaire sur le même emplacement de mémoire ou à un calcul de valeur utilisant la valeur d'un objet quelconque dans le même emplacement de mémoire, et s'ils ne sont pas potentiellement concurrents, le comportement est indéfini.

Dónde valeur est défini à [N4659 type.de.base]

Pour les types trivialement copiables, la représentation de la valeur est un ensemble de bits dans la représentation de l'objet qui détermine un valeur qui est un élément discret d'un ensemble de valeurs défini par la mise en œuvre.

Desde [N3337 basic.exec]

Sauf indication contraire, les évaluations des opérandes des opérateurs individuels et des sous-expressions des 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 est indéfini.

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

Pour les types trivialement copiables, la représentation de la valeur est un ensemble de bits dans la représentation de l'objet qui détermine un valeur qui est un élément discret d'un ensemble de valeurs défini par la mise en œuvre.

Ils sont identiques, à l'exception de la mention de la concurrence, qui n'a pas d'importance, et de l'utilisation de l'attribut emplacement de mémoire au lieu de objet scalaire

Types arithmétiques, types d'énumération, types de pointeurs, types de pointeurs vers les membres, std::nullptr_t Les versions qualifiées cv de ces types sont collectivement appelées types scalaires.

Ce qui n'affecte pas l'exemple.

Desde [N4659 expr.ass]

L'opérateur d'affectation (=) et les opérateurs d'affectation composés regroupent tous de droite à gauche. Ils requièrent tous une lvalue modifiable comme opérande gauche et retournent une lvalue faisant référence à l'opérande gauche. Dans tous les cas, le résultat 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 de droite et de gauche, et avant le calcul de la valeur de l'expression d'affectation. L'opérande de droite est séquencé avant l'opérande de gauche.

Desde [N3337 expr.ass]

L'opérateur d'affectation (=) et les opérateurs d'affectation composés regroupent tous de droite à gauche. Ils requièrent tous une lvalue modifiable comme opérande gauche et retournent une lvalue faisant référence à l'opérande gauche. Dans tous les cas, le résultat 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 de droite et de gauche, et avant le calcul de la valeur de l'expression d'affectation.

La seule différence est que la dernière phrase est absente dans N3337.

La dernière phrase, cependant, ne devrait pas avoir d'importance car l'opérande gauche i n'est ni "un autre effet secondaire" ni "en utilisant la valeur du même objet scalaire" comme le id-expression est une lvalue.

23 votes

Vous avez identifié la raison pour laquelle : En C++17, l'opérande de droite est séquencé avant l'opérande de gauche. En C++11, cette séquence n'existait pas. Quelle est, précisément, votre question ?

4 votes

@Rob Voir la dernière phrase.

0 votes

Il y a deux effets secondaires non séquencés, tous deux écrits : i = a pour effet secondaire d'écrire dans i . i++ a pour effet secondaire d'écrire dans i .

150voto

AndreyT Points 139512

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

Le seul espoir dans ce cas est celui des garanties supplémentaires apportées par les opérateurs spécifiques utilisés dans l'ERS. Si le RHS a utilisé un préfixe ++ Les propriétés de séquençage spécifiques à la forme préfixe de la ++ aurait sauvé la mise dans cet exemple. Mais postfix ++ est une autre histoire : il ne donne pas de telles garanties. En C++11, les effets secondaires de la fonction = et postfix ++ se retrouvent non séquencés les uns par rapport aux autres dans cet exemple. Et c'est l'UB.

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

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

En combinaison avec ce qui précède, cela constitue une garantie très solide. Elle enchaîne tout qui se produit dans le RHS (y compris les éventuels effets secondaires) avant que tout qui se produit dans le LHS. Puisque l'affectation réelle est séquencée après LHS (et RHS), ce séquençage supplémentaire isole complètement l'acte d'affectation 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 tenir compte des commentaires de @John Bollinger).

3 votes

Est-il vraiment correct d'inclure "l'acte réel d'affectation" dans les effets couverts par "l'opérande de gauche" dans cet extrait ? La norme contient un langage distinct sur le séquençage de l'affectation réelle. Je considère que l'extrait que vous avez présenté a une portée limitée au séquençage des sous-expressions de gauche et de droite, ce qui ne semble pas suffisant, en combinaison avec le reste de cette section, pour soutenir la définition de l'énoncé de l'OP.

12 votes

Correction : l'assignation réelle est toujours séquencée après le calcul de la valeur de l'opérande de gauche, et l'évaluation de l'opérande de gauche est séquencée après l'évaluation (complète) de l'opérande de droite, donc oui, ce changement est suffisant pour soutenir le caractère bien défini demandé par le PO. Je ne fais qu'ergoter sur les détails, donc, mais ceux-ci ont de l'importance, car ils peuvent avoir des implications différentes pour des codes différents.

0 votes

@JohnBollinger : Je pense que garantir la séquence de l'évaluation de la valeur l du côté gauche pourrait nuire inutilement à l'efficacité de toute génération de code simple et directe. L'évaluation directe de *foo() = globalVariable; par exemple, invoquerait foo() puis aller chercher globalVariable et le stocker dans le pointeur reçu de foo() . Saisir la valeur de globalVariable avant l'appel si le code n'en a pas besoin ajouterait un coût ; si le code a besoin de la variable avant l'appel, la copier dans une variable temporaire ne devrait pas ajouter de coût par rapport à la sémantique imposée.

33voto

Vous avez identifié la nouvelle phrase

L'opérande de droite est séquencé avant l'opérande de 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és avant l'affectation. En C++11, seul le calcul de la valeur de l'opérande de droite a été séquencé avant l'affectation.

10voto

Lundin Points 21616

Dans les anciennes normes C++ et dans la 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 que, par conséquent, le comportement est indéfini s'ils utilisent la même variable.

Ce texte a simplement été supprimé dans C++11, ce qui laisse une certaine ambiguïté. S'agit-il d'UB ou non ? Ceci a été clarifié dans C++17 où ils ont ajouté :

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


À titre d'information, dans des normes encore plus anciennes, tout cela était très clair, par exemple dans la norme C99 :

L'ordre d'évaluation des opérandes n'est pas spécifié. Si l'on tente de modifier le résultat le résultat d'un opérateur d'assignation ou d'y accéder après le point de séquence suivant, le le comportement est indéfini.

En fait, dans C11/C++11, ils ont fait une erreur en supprimant ce texte.

3voto

Matt McNabb Points 14273

Il s'agit d'un complément d'information aux autres réponses et je le poste car le code ci-dessous est également souvent 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++;

El + 1 est un faux-fuyant et la raison pour laquelle la norme l'a utilisé dans ses exemples n'est pas vraiment claire, bien que je me souvienne que des personnes ont argumenté sur des listes de diffusion avant C++11 que peut-être l'élément + 1 a fait une différence en forçant une conversion précoce des valeurs l du côté droit. Rien de tout cela n'est applicable en C++17 (et n'a probablement jamais été appliqué dans aucune version de C++).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X