47 votes

En C++11, est-ce que `i += ++i + 1` a un comportement indéfini ?

Cette question s'est posée alors que je lisais (les réponses à) Pourquoi i = ++i + 1 est-il bien défini en C++11 ?

J'en déduis que l'explication subtile est que (1) l'expression ++i renvoie une valeur l, mais + prend des valeurs pr comme opérandes, de sorte qu'une conversion de valeur l en valeur pr doit être effectuée ; cela implique d'obtenir la valeur actuelle de cette valeur l (plutôt qu'une valeur de plus que l'ancienne valeur de i ) et doivent donc être séquencés après l'effet secondaire de l'incrémentation (c'est-à-dire la mise à jour de l'information). i ) (2) la valeur LHS de l'affectation est également une valeur l, de sorte que l'évaluation de sa valeur n'implique pas la récupération de la valeur actuelle de i ; bien que ce calcul de valeur ne soit pas séquencé par rapport au calcul de valeur de la RHS, cela ne pose pas de problème (3) le calcul de valeur de l'affectation elle-même implique une mise à jour de la valeur de la RHS, ce qui n'est pas le cas de la RHS. i (à nouveau), mais elle est séquencée après le calcul de la valeur de son RHS, et donc après la mise à jour précédente de i ; pas de problème.

D'accord, il n'y a donc pas d'UB. Ma question est la suivante : que se passerait-il si l'on changeait l'opérateur d'affectation de = a += (ou un opérateur similaire).

L'évaluation de l'expression i += ++i + 1 conduisent à un comportement indéfini ?

À mon avis, la norme semble se contredire ici. Puisque la LHS de += est toujours une valeur l (et son RHS toujours une valeur pr), le même raisonnement que ci-dessus s'applique en ce qui concerne (1) et (2) ; il n'y a pas de comportement indéfini dans l'évaluation des opérandes sur += . Comme pour (3), l'opération de l'affectation composée += (plus précisément l'effet secondaire de cette opération ; le calcul de sa valeur, s'il est nécessaire, est de toute façon séquencé après son effet secondaire) doit maintenant à la fois récupérer la valeur actuelle de i , et ensuite (évidemment placé après lui, même si la norme ne le dit pas explicitement, sinon l'évaluation de ces opérateurs serait siempre invoquer un comportement indéfini) ajouter le RHS et stocker le résultat dans i . Ces deux opérations auraient donné lieu à un comportement indéfini si elles n'avaient pas été séquencées par rapport à l'effet secondaire de la commande ++ mais, comme nous l'avons vu plus haut (l'effet secondaire de la ++ est séquencée avant le calcul de la valeur de + donnant le RHS de la += dont le calcul de la valeur est séquencé avant l'opération de cette affectation composée), ce n'est pas le cas.

Mais d'un autre côté, la norme stipule également que E += F est équivalent à E = E + F sauf que (la valeur l) E n'est évaluée qu'une seule fois. Dans notre exemple, le calcul de la valeur de i (ce qui est le cas de l E est ici) en tant que lvalue n'implique rien qui doive être séquencé par rapport à d'autres actions, donc le faire une ou deux fois ne fait pas de différence ; notre expression devrait être strictement équivalente à E = E + F . Mais voici le problème : il est assez évident que l'évaluation de l'efficacité d'un produit ou d'un service ne peut se faire qu'à partir d'un certain nombre de critères. i = i + (++i + 1) aurait un comportement indéfini ! Qu'est-ce qui donne ? Ou s'agit-il d'un défaut de la norme ?

Ajouté. J'ai légèrement modifié ma discussion ci-dessus, afin de rendre plus justice à la distinction entre les effets secondaires et les calculs de valeur, et d'utiliser "l'évaluation" (comme le fait la norme) d'une expression pour englober les deux. Je pense que ma principale interrogation ne porte pas seulement sur la question de savoir si un comportement est défini ou non dans cet exemple, mais sur la façon dont il faut lire la norme pour en décider. Notamment, doit-on considérer l'équivalence de E op= F a E = E op F comme autorité ultime pour la sémantique de l'opération d'affectation composée (auquel cas l'exemple a clairement UB), ou simplement comme une indication de l'opération mathématique impliquée dans la détermination de la valeur à affecter (à savoir celle identifiée par op avec la valeur lHS convertie en valeur r de l'opérateur d'affectation composé comme opérande gauche et sa valeur RHS comme opérande droite). Cette dernière option rend beaucoup plus difficile l'argumentation en faveur de l'UB dans cet exemple, comme j'ai essayé de l'expliquer. J'admets qu'il est tentant de faire autorité sur l'équivalence (de sorte que les assignations composées deviennent une sorte de primitives de deuxième classe, dont la signification est donnée par la réécriture en termes de primitives de première classe ; la définition du langage serait ainsi simplifiée), mais il y a des arguments assez forts contre cela :

  • L'équivalence n'est pas absolue, en raison de la " E n'est évalué qu'une seule fois". Notez que cette exception est essentielle pour éviter toute utilisation où l'évaluation de E implique un effet secondaire de comportement indéfini, par exemple dans le cas assez courant de l'application a[i++] += b; l'utilisation. En fait, je pense qu'aucune réécriture absolument équivalente n'est possible pour éliminer les affectations composées. ||| pour désigner les évaluations non séquencées, on pourrait essayer de définir un opérateur E op= F; (avec int pour simplifier) comme équivalent à { int& L=E ||| int R=F; L = L + R; } mais l'exemple n'a plus d'UB. En tout état de cause, la norme ne nous donne aucune recette de réécriture.

  • La norme ne traite pas les affectations composées comme des primitives de seconde classe pour lesquelles aucune définition distincte de la sémantique n'est nécessaire. Par exemple, au point 5.17 (c'est moi qui souligne)

    L'opérateur d'affectation (=) et les opérateurs d'affectation composés sont tous groupés de droite à gauche. [...] Dans tous les cas , l'affectation des opérandes de droite et de gauche, et avant le calcul de la valeur de l'expression d'affectation. 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 .

  • Si l'intention était de laisser les affectations composées être de simples raccourcis pour les affectations simples, il n'y aurait aucune raison de les inclure explicitement dans cette description. La dernière phrase contredit même directement ce qui serait le cas si l'équivalence était considérée comme faisant autorité.

Si l'on admet que les affectations composées ont une sémantique qui leur est propre, le problème se pose alors que leur évaluation implique (outre l'opération mathématique) plus qu'un effet de bord (l'affectation) et une évaluation de la valeur (séquencée après l'affectation), mais aussi une opération non nommée de recherche de la valeur (précédente) de la LHS. Cette opération devrait normalement être traitée dans le cadre de la "conversion de valeur l à valeur r", mais il est difficile de la justifier ici, étant donné qu'il n'y a pas d'opérateur qui prenne la valeur LHS comme valeur r (bien qu'il y en ait un dans la forme étendue "équivalente"). C'est précisément cette opération non nommée dont la relation potentielle non séquencée avec l'effet secondaire de ++ provoquerait l'UB, mais cette relation non séquencée n'est nulle part explicitement mentionnée dans la norme, car l'opération non nommée ne l'est pas. Il est difficile de justifier l'utilisation d'une opération dont l'existence même n'est qu'implicite dans la norme.

16voto

dyp Points 19641

A propos de la description de i = ++i + 1

J'en déduis que l'explication subtile est que

(1) l'expression ++i renvoie une valeur l, mais + prend des valeurs pr comme opérandes, de sorte qu'une conversion de valeur l en valeur pr doit être effectuée ;

Probablement, voir Numéro actif du GTC 1642 .

Il s'agit de l'obta valeur actuelle de cette valeur l (plutôt qu'une valeur de i ) et doit donc être séquencée après l'effet secondaire de la (c'est-à-dire la mise à jour de i )

Le séquençage est ici défini pour l'incrément (indirectement, par l'intermédiaire de += , voir (a) ) : L'effet secondaire de la ++ (la modification de i ) est séquencée avant le calcul de la valeur de l'expression entière ++i . Ce dernier se réfère à calculer le résultat de ++i et non à charger la valeur de i .

(2) la LHS de l'assi lvalue, donc l'évaluation de sa valeur n'implique pas d'aller chercher la valeur actuelle de valeur actuelle de i ; alors que ce calcul de valeur n'est pas séquencé par rapport au calcul de valeur du RHS. calcul de la valeur de la RHS, cela ne pose pas de probleme

Je ne pense pas que cela soit correctement défini dans la norme, mais je suis d'accord.

(3) la valeur de l'affectation elle-même implique une mise à jour i (encore),

Le calcul de la valeur de i = expr n'est nécessaire que lorsque vous utilisez le résultat, par exemple int x = (i = expr); o (i = expr) = 42; . Le calcul de la valeur lui-même ne modifie pas i .

La modification de i dans l'expression i = expr qui se produit en raison de la = est appelé effet secondaire de = . Cet effet secondaire est séquencé avant le calcul de la valeur de i = expr -- ou plutôt le calcul de la valeur de i = expr est séquencée après l'effet secondaire de l'affectation dans i = expr .

En général, le calcul de la valeur des opérandes d'une expression est séquencé avant l'effet secondaire de cette expression, bien entendu.

mais est séquencée après le calcul de la valeur de sa RHS, et donc après le calcul de la valeur de sa RHS. la mise à jour précédente de i ; pas de problème.

En effet secondaire de l'affectation i = expr est séquencée après le calcul de la valeur des opérandes i (A) et expr de la mission.

En expr dans ce cas est un + -expression : expr1 + 1 . Le calcul de la valeur de cette expression est séquencé après le calcul de la valeur de ses opérandes expr1 y 1 .

En expr1 voici ++i . Le calcul de la valeur de ++i est séquencée après l'effet secondaire de ++i (la modification de i ) (B)

C'est pourquoi i = ++i + 1 est sûr : Il y a une chaîne de séquencé avant entre le calcul de la valeur en (A) et l'effet de bord sur la même variable en (B).


(a) La norme définit ++expr en termes de expr += 1 qui est définie comme suit expr = expr + 1 avec expr n'étant évaluée qu'une seule fois.

Pour ce faire expr = expr + 1 Nous n'avons donc qu'un seul calcul de la valeur de expr . L'effet secondaire de = est séquencée avant le calcul de la valeur de l'ensemble des expr = expr + 1 et il est séquencé après le calcul de la valeur des opérandes expr (LHS) et expr + 1 (RHS).

Cela correspond à mon affirmation selon laquelle, pour les ++expr l'effet secondaire est séquencé avant le calcul de la valeur de ++expr .


A propos de i += ++i + 1

Le calcul de la valeur de i += ++i + 1 impliquent un comportement indéfini ?

S LHS de += est toujours une valeur l (et sa RHS est toujours une valeur pr), la même valeur raisonnement que ci-dessus s'applique en ce qui concerne (1) et (2) ; quant à (3) le calcul de la valeur de la += L'opérateur doit maintenant à la fois valeur actuelle de i et ensuite (évidemmentl la norme ne le dit pas explicitement, sinon l'exécution de ces opérateurs serait de tels opérateurs provoquerait toujours un comportement indéfini) l'exécution de l'opération l'addition du RHS et stocke le résultat dans le champ i .

Je pense que le problème est le suivant : l'addition de la valeur des i obtenu à partir de la LHS de i += au résultat de ++i + 1 nécessite de charger la valeur de i . Cette charge n'est pas séquencée par rapport à la modification effectuée par ++i . C'est essentiellement ce que vous dites dans votre description alternative, après la réécriture imposée par la norme i += expr -> i = i + expr . Ici, le calcul de la valeur de i n'est pas séquencée par rapport au calcul de la valeur de expr . C'est là que vous obtenez l'UB .


Une autre approche pour i += ++i + 1

le calcul de la valeur de la += L'opérateur doit maintenant valeur actuelle de i , et entonces [...] effectuer l'addition des RHS

L'ERS étant ++i + 1 . Le calcul du résultat de cette expression peut être effectué "en même temps" que le chargement de la valeur de i à partir de la LHS. Ainsi, le mot entonces dans cette phrase est trompeuse : Bien sûr, il doit d'abord se charger i et y ajouter le résultat de l'ERS. Mais le calcul de l'ERS peut être effectué "en même temps" que la charge de i Vous pouvez donc obtenir l'ancienne ou la nouvelle valeur de i tel que modifié par l'ERS.

En général, un magasin et un chargement "simultané" constituent une course aux données, ce qui entraîne un comportement non défini.

"concomitamment" = = non séquencé ; non séquencé peut impliquer qu'il est exécuté simultanément Nous pouvons donc utiliser concurrence pour montrer un résultat possible


Adresser l'addendum

à l'aide d'un ||| pour désigner les évaluations non séquencées, on pourrait essayer de définir un opérateur E op= F; (avec des opérandes int pour simplifier) comme équivalent à { int& L=E ||| int R=F; L = L + R; } mais l'exemple n'a plus d'UB.

Laisser E être i y F être ++i (nous n'avons pas besoin de la + 1 ). Alors, pour i = ++i

int* lhs_address;
int lhs_value;
int* rhs_address;
int rhs_value;

    (         lhs_address = &i)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);

*lhs_address = rhs_value;

En revanche, pour les i += ++i

    (         lhs_address = &i, lhs_value = *lhs_address)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);

int total_value = lhs_value + rhs_value;
*lhs_address = total_value;

Ceci est censé représenter ma compréhension des garanties de séquençage. Notez que les , séquence tous les calculs de valeur et les effets secondaires de la LHS avant ceux de la RHS. Les parenthèses n'affectent pas le séquencement. Dans le second cas, i += ++i , nous avons une modification de i non séquencée par rapport à une conversion de valeur l à valeur r de i => UB.

La norme ne traite pas les affectations composées comme des primitives de deuxième classe pour lesquelles aucune définition distincte de la sémantique n'est nécessaire.

Je dirais qu'il s'agit d'une redondance. La réécriture de E1 op = E2 a E1 = E1 op E2 inclut également les types d'expression et les catégories de valeurs requis (sur la droite, 5.17/1 dit quelque chose sur la gauche), ce qu'il advient des types de pointeurs, les conversions requises, etc. Ce qui est triste, c'est que la phrase "En ce qui concerne un " dans 5.17/1 ne figure pas dans 5.17/7 en tant qu'exception à cette équivalence.

Quoi qu'il en soit, je pense que nous devrions comparer les garanties et les exigences de l'affectation composée par rapport à l'affectation simple plus l'opérateur, et voir s'il y a une contradiction.

Une fois que nous aurons ajouté la mention "en ce qui concerne un " à la liste des exceptions de l'article 5.17/7, je ne pense pas qu'il y ait de contradiction.

En fait, comme vous pouvez le voir dans la discussion de la réponse de Marc van Leeuwen, cette phrase conduit à l'observation intéressante suivante :

int i; // global
int& f() { return ++i; }
int main() {
    i  = i + f(); // (A)
    i +=     f(); // (B)
}

Il semble que le point (A) ait deux issues possibles, puisque l'évaluation du corps des f est séquencée de manière indéterminée avec le calcul de la valeur de la i en i + f() .

En revanche, en (B), l'évaluation du corps des f() est séquencée avant le calcul de la valeur de i puisque += doit être considérée comme une opération unique, et f() doit certainement être évaluée avant l'affectation de += .

5voto

Shafik Yaghmour Points 42198

L'expression :

i += ++i + 1

invoque un comportement non défini. La méthode de l'avocat de la langue nous oblige à revenir au rapport de défaut qui aboutit à :

i = ++i + 1 ;

devient bien défini en C++11, qui est rapport de défectuosité 637. Règles de séquençage et exemple de désaccord Il commence par dire :

Dans la version 1.9 [intro.execution] paragraphe 16, l'expression suivante est l'expression suivante est toujours citée comme un exemple de comportement non défini :

i = ++i + 1;

Toutefois, il semble que les nouvelles règles de séquençage rendent cette expression bien définie

La logique utilisée dans le rapport est la suivante :

  1. L'effet de bord de l'affectation doit être séquencé après les calculs de valeur de sa LHS et de sa RHS (5.17 [expr.ass] paragraphe 1).

  2. Le LHS (i) est une valeur l, de sorte que le calcul de sa valeur implique le calcul de l'adresse de i.

  3. Pour calculer la valeur de l'expression RHS (++i + 1), il faut d'abord calculer la valeur de l'expression lvalue ++i, puis effectuer une conversion lvalue-valeur sur le résultat. Cela garantit que l'effet de bord d'incrémentation est séquencé avant le calcul de l'opération d'addition, qui à son tour est séquencé avant l'effet de bord d'affectation. En d'autres termes, cela permet d'obtenir un ordre et une valeur finale bien définis pour cette expression.

Ainsi, dans cette question, notre problème modifie la RHS qui va de :

++i + 1

à :

i + ++i + 1

en raison de projet de norme C++11 section 5.17 Opérateurs d'affectation et d'affectation composée qui dit :

Le comportement d'une expression de la forme E1 op = E2 est l'équation suivante E1 = E1 op E2 sauf que E1 n'est évaluée qu'une seule fois. [...]

Nous nous trouvons donc dans une situation où le calcul de i en el RHS n'est pas séquencée par rapport à ++i et nous avons donc un comportement indéfini. Cela découle de la section 1.9 paragraphe 15 qui dit :

Sauf indication contraire, les évaluations d'opérandes indivi et des sous-expressions d'expressions individuelles ne sont pas séquencées. [ Note : Dans une expression qui est évaluée plus d'une fois au cours de l'opération l'exécution d'un programme, les évaluations non séquencées et séquencées de manière indéterminée de ses sous-expressions ne sont pas séquencées. de ses sous-expressions n'ont pas besoin d'être effectuées de manière cohérente dans les différentes évaluations. -Fin de la note ] Les calculs de valeur des opérandes d'un opérateur sont séquencés avant le calcul de la valeur des le calcul de la valeur du résultat de l'opérateur. Si un effet de bord sur un objet scalaire est n'est pas séquencé par rapport à un autre effet de bord sur le même objet scalaire. ou à un calcul de valeur utilisant la valeur du même objet scalaire le comportement est indéfini.

La façon la plus pragmatique de montrer cela serait d'utiliser clang pour tester le code, ce qui génère l'avertissement suivant ( voir en direct ) :

warning: unsequenced modification and access to 'i' [-Wunsequenced]
i += ++i + 1 ;
  ~~ ^

pour ce code :

int main()
{
    int i = 0 ;

    i += ++i + 1 ;
}

Ceci est encore renforcé par cet exemple de test explicite dans le document clang's suite de tests pour -Non séquencé :

 a += ++a;

1voto

MWid Points 1373

Oui, c'est l'UB !

L'évaluation de votre expression

i += ++i + 1

se déroule selon les étapes suivantes :

5.17p1 (C++11) :

L'opérateur d'affectation (=) et les opérateurs d'affectation composés sont tous groupés de droite à gauche. Tous requièrent une valeur l modifiable comme opérande de gauche et renvoient une valeur l faisant référence à l'opérande de gauche. Dans tous les cas, le résultat est un champ binaire si l'opérande de gauche est un champ binaire. 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.

Que signifie "calcul de la valeur" ?

1,9p12 donne la réponse :

L'accès à un objet désigné par une glvalue volatile (3.10), la modification d'un objet, l'appel à une fonction d'E/S de la bibliothèque ou l'appel à une fonction qui effectue l'une de ces opérations sont autant d'effets de bord, c'est-à-dire des modifications de l'état de l'environnement d'exécution. L'évaluation d'une expression (ou d'une sous-expression) comprend en général à la fois 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) et le déclenchement d'effets secondaires.

Puisque votre code utilise un opérateur d'affectation composé L'article 5.17p7 nous indique comment cet opérateur se comporte :

Le comportement d'une expression de la forme E1 op= E2 est équivalent à E1 = E1 op E2 except that E1 n'est évalué qu'une seule fois.

Par conséquent, l'évaluation de l'expression E1 ( == i) implique les deux, en déterminant l'identité de l'objet désigné par i et un lvalue-to-rvalue pour récupérer la valeur stockée dans cet objet. Mais l'évaluation des deux opérandes E1 y E2 ne sont pas séquencés les uns par rapport aux autres. Nous obtenons donc comportement non défini puisque l'évaluation de E2 ( == ++i + 1) déclenche un effet secondaire (mise à jour i ).

1.9p15 :

... Si un effet de bord sur un objet scalaire n'est pas séquencé par rapport à soit un autre effet de bord sur le même objet scalaire ou un calcul de valeur utilisant la valeur du même objet scalaire, le comportement est indéfini.


Les affirmations suivantes de votre question/commentaire semblent être à l'origine de votre incompréhension :

(2) la valeur LHS de l'affectation est également une valeur l, de sorte que l'évaluation de sa valeur n'implique pas la récupération de la valeur actuelle de i

La recherche d'une valeur peut faire partie de l'évaluation d'une valeur. Mais dans E += F, la seule prvaleur est F, de sorte que la recherche de la valeur de E ne fait pas partie de l'évaluation de la sous-expression (lvaleur) E.

Le fait qu'une expression soit une lvaleur ou une rvaleur ne dit rien sur la manière dont cette expression doit être évaluée. Certains opérateurs requièrent des lvaleurs comme opérandes, d'autres des rvaleurs.

Clause 5p8 :

Lorsqu'une expression glvalue apparaît en tant qu'opérande d'un opérateur qui attend une valeur pr pour cet opérande, les conversions standard lvalue-valeur (4.1), tableau-pointeur (4.2) ou fonction-pointeur (4.3) sont appliquées pour convertir l'expression en valeur pr.

Dans une affectation simple, l'évaluation de la LHS ne nécessite que la détermination de l'identité de l'objet. Mais dans une affectation composée telle que += le LHS doit être une lvaleur modifiable, mais l'évaluation du LHS dans ce cas consiste à déterminer l'identité de l'objet et à effectuer une conversion de lvaleur à rvaleur. C'est le résultat de cette conversion (qui est une prvalue) qui est ajouté au résultat (également une prvalue) de l'évaluation du RHS.

"But in E += F the only prvalue is F so fetching the value of E is not part of the evaluation of the (lvalue) subexpression E" (Mais dans E += F, la seule valeur est F, donc la recherche de la valeur de E ne fait pas partie de l'évaluation de la sous-expression E)

Ce n'est pas vrai, comme je l'ai expliqué plus haut. Dans votre exemple F est une expression prvalue, mais F peut tout aussi bien être une expression lvalue. Dans ce cas, la conversion de lvalue en rvalue est également appliquée à F . 5.17p7 cité ci-dessus nous indique la sémantique des opérateurs d'affectation composés. La norme stipule que l'opérateur comportement de E += F est le même que celui de E = E + F mais E n'est évaluée qu'une seule fois. Ici, l'évaluation de E inclut la conversion de lvalue en rvalue, car l'opérateur binaire + exige que ses opérandes soient des valeurs r.

0voto

gnasher729 Points 5011

Du point de vue de l'auteur du compilateur, il ne se soucie pas de "i += ++i + 1", car quoi que fasse le compilateur, le programmeur n'obtiendra peut-être pas le bon résultat, mais il aura certainement ce qu'il mérite. Et personne n'écrit de code comme cela. Ce qui intéresse le rédacteur du compilateur, c'est

*p += ++(*q) + 1;

Le code doit lire *p et *q, augmenter *q de 1 et augmenter *p d'un montant calculé. Ici, l'auteur du compilateur se soucie des restrictions relatives à l'ordre des opérations de lecture et d'écriture. Il est évident que si p et q pointent vers des objets différents, l'ordre ne fait aucune différence, mais si p = q, il en fera une. Là encore, p sera différent de q, à moins que le programmeur qui écrit le code ne soit fou.

En rendant le code indéfini, le langage permet au compilateur de produire le code le plus rapide possible sans se soucier des programmeurs fous. En définissant le code, le langage oblige le compilateur à produire un code conforme à la norme, même dans les cas de folie, ce qui peut le rendre plus lent. Les auteurs de compilateurs et les programmeurs sains d'esprit n'aiment pas cela.

Ainsi, même si le comportement est défini en C++11, il serait très dangereux de l'utiliser, car (a) un compilateur pourrait ne pas être modifié par rapport au comportement de C++03, et (b) il pourrait s'agir d'un comportement non défini en C++14, pour les raisons susmentionnées.

0voto

Marc van Leeuwen Points 803

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)

  1. 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 [...]

  2. 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, ...

  3. 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.

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