Avant de commencer à crier un comportement indéfini, ceci 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 rassembler, 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 la 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 du même emplacement de mémoire et s'ils ne sont pas potentiellement concurrents, le comportement est indéfini.
Où la valeur est définie à [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 l'un des éléments discrets d'une définition d'implémentation. ensemble de valeurs
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 la 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 valeur est un ensemble de bits dans la représentation d'objet qui détermine une valeur , qui est l'un des éléments discrets d'une définition d'implémentation. ensemble de valeurs.
Ils sont identiques sauf la mention de la simultanéité qui importe peu, et avec l’utilisation de emplacement mémoire au lieu de scalaire objet , où
Les types arithmétiques, types d'énumération, types de pointeur, pointeur sur les types de membre,
std::nullptr_t
et les versions qualifiées de cv de ces types sont collectivement appelés types scalaires.
Ce qui n'affecte pas l'exemple.
L'opérateur d'affectation (=) et les opérateurs d'affectation composés regroupent tous les groupes 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.
L'opérateur d'affectation (=) et les opérateurs d'affectation composés regroupent tous les groupes 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 manquante dans N3337.
La dernière phrase ne devrait cependant 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" que id-expression est une lvalue .
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 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 la RHS. Cela ne dit rien sur les effets secondaires qui pourraient être présents dans l'ERS, car la survenue d'effets secondaires ne fait pas partie de calcul de la valeur. Les exigences de C++ 11 n'établissent pas de séquence relative entre l'acte d'assignation et les effets secondaires éventuels de la RHS. C'est ce qui crée le potentiel pour UB.
Dans ce cas, le seul espoir réside dans les éventuelles garanties supplémentaires fournies par des opérateurs spécifiques utilisés dans RHS. Si le RHS utilisait un préfixe ++
, les propriétés de séquencement spécifiques à la forme de préfixe ++
auraient sauvé la journée dans cet exemple. Mais postfixe ++
est une histoire différente: il ne fait pas de telles garanties. En C++ 11, les effets secondaires de =
et de postfixe ++
se retrouvent sans séquence 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, il constitue une garantie très solide. Il séquence tout ce qui se passe dans le RHS (y compris tous les effets secondaires) avant tout ce qui se passe dans le LHS. Étant donné que 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’attribution de tout effet secondaire présent dans RHS. Cette séquence plus forte est ce qui élimine le UB ci-dessus.
(Mise à jour pour prendre en compte les commentaires de @John Bollinger.)
Vous avez identifié la nouvelle phrase
L'opérande droit est séquencé avant l'opérande gauche.
et vous avez correctement identifié le fait que l’évaluation de l’opérande gauche en tant que valeur est sans importance. Cependant, séquence avant est spécifié pour être une relation transitive. L’opérande droit complet (y compris la post-incrémentation) est donc aussi 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.
Dans les normes C++ plus anciennes et dans C11, la définition du texte de l'opérateur d'affectation se termine par le texte suivant:
Les évaluations des opérandes ne sont pas séquencées.
Ce qui signifie que les effets secondaires dans les opérandes ne sont pas séquencés et qu’ils ont donc définitivement 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 est indéterminé. Si vous tentez de modifier le résultat d'un opérateur d'affectation ou d'y accéder après le point de séquence suivant, le comportement n'est pas défini.
Fondamentalement, dans C11/C++ 11, ils ont tout gâché en supprimant ce texte.