Est-il légal d’utiliser l’opérateur d’incrémentation dans un appel de fonction C ++?

Il y a eu un débat sur cette question pour savoir si le code suivant est légal C ++:

std::list::iterator i = items.begin(); while (i != items.end()) { bool isActive = (*i)->update(); if (!isActive) { items.erase(i++); // *** Is this undefined behavior? *** } else { other_code_involving(*i); ++i; } } 

Le problème ici est que erase() invalidera l’iterator en question. Si cela se produit avant que i++ soit évalué, l’incrémentation que j’aime est un comportement techniquement non défini, même si cela semble fonctionner avec un compilateur particulier. Un côté du débat dit que tous les arguments de fonction sont entièrement évalués avant l’appel de la fonction. L’autre partie dit, “la seule garantie est que i ++ se produira avant la prochaine instruction et après l’utilisation de i ++. Le fait que ce soit avant l’effacement (i ++) soit appelé ou après le compilateur.”

J’ai ouvert cette question pour espérer régler ce débat.

Quoth le standard C ++ 1.9.16:

Lors de l’appel d’une fonction (que la fonction soit ou non en ligne), chaque calcul de valeur et effet secondaire associé à toute expression d’argument ou à l’expression postfixée désignant la fonction appelée est séquencé avant l’exécution de chaque expression ou déclaration du corps appelé fonction. (Remarque: les calculs de valeur et les effets secondaires associés aux différentes expressions d’argument ne sont pas séquencés.)

Donc, il me semble que ce code:

 foo(i++); 

est parfaitement légal Il va incrémenter i puis appeler foo avec la valeur précédente de i . Cependant, ce code:

 foo(i++, i++); 

produit un comportement indéfini car le paragraphe 1.9.16 dit également:

Si un effet secondaire sur un object scalaire est non séquencé par rapport à un autre effet secondaire sur le même object scalaire ou à un calcul de valeur utilisant la valeur du même object scalaire, le comportement est indéfini.

Sur la base de la réponse de Kristo ,

 foo(i++, i++); 

génère un comportement indéfini car l’ordre dans lequel les arguments de fonction sont évalués n’est pas défini (et dans le cas plus général, si vous lisez une variable deux fois dans une expression où vous l’écrivez également, le résultat n’est pas défini). Vous ne savez pas quel argument sera incrémenté en premier.

 int i = 1; foo(i++, i++); 

pourrait entraîner un appel de fonction de

 foo(2, 1); 

ou

 foo(1, 2); 

ou même

 foo(1, 1); 

Exécutez la commande suivante pour voir ce qui se passe sur votre plate-forme:

 #include  using namespace std; void foo(int a, int b) { cout << "a: " << a << endl; cout << "b: " << b << endl; } int main() { int i = 1; foo(i++, i++); } 

Sur ma machine je reçois

 $ ./a.out a: 2 b: 1 

à chaque fois, mais ce code n'est pas portable , donc je m'attendrais à voir des résultats différents avec différents compilateurs.

La norme indique que l’effet secondaire se produit avant l’appel, le code est donc le même que:

 std::list::iterator i_before = i; i = i_before + 1; items.erase(i_before); 

plutôt que d’être:

 std::list::iterator i_before = i; items.erase(i); i = i_before + 1; 

Donc, c’est sûr dans ce cas, parce que list.erase () n’invalide spécifiquement aucun iterator autre que celui effacé.

Cela dit, c’est un mauvais style – la fonction d’effacement de tous les conteneurs renvoie spécifiquement l’iterator suivant, de sorte que vous n’avez pas à vous soucier d’invalider les iterators en raison d’une réallocation.

 i = items.erase(i); 

sera sûr pour les listes, et sera également sans danger pour les vecteurs, les deques et tout autre conteneur de séquence si vous souhaitez modifier votre stockage.

Vous n’auriez pas non plus le code original à comstackr sans avertissements – il faudrait écrire

 (void)items.erase(i++); 

pour éviter un avertissement sur un retour inutilisé, ce qui serait un indice important que vous faites quelque chose de bizarre.

C’est parfaitement correct. La valeur passée serait la valeur de “i” avant l’incrément.

Pour construire la réponse de MarkusQ:;)

Ou plutôt, le commentaire de Bill:

( Edit: Aw, le commentaire est reparti … Eh bien)

Ils sont autorisés à être évalués en parallèle. Que cela se produise ou non dans la pratique est techniquement parlant non pertinent.

Vous n’avez pas besoin de parallélisme de thread pour que cela se produise, évaluez simplement la première étape des deux (prenez la valeur de i) avant la seconde (incrément i). Parfaitement légal, et certains compilateurs peuvent considérer qu’il est plus efficace que d’évaluer pleinement un i ++ avant de commencer le second.

En fait, je m’attendrais à ce que ce soit une optimisation commune. Regardez-le d’un sharepoint vue de la planification des instructions. Vous devez évaluer ce qui suit:

  1. Prenez la valeur de i pour le bon argument
  2. Incrémenter i dans le bon argument
  3. Prenez la valeur de i pour l’argument de gauche
  4. Incrémenter i dans l’argument de gauche

Mais il n’y a vraiment aucune dépendance entre l’argument de gauche et celui de droite. L’évaluation des arguments se déroule dans un ordre non spécifié, et n’a pas besoin d’être effectuée de manière séquentielle (ce qui explique pourquoi new () dans les arguments de fonction est généralement une fuite de mémoire, même dans un pointeur intelligent). deux fois dans la même expression. Nous avons cependant une dépendance entre 1 et 2, et entre 3 et 4. Alors, pourquoi le compilateur attend-il que 2 se termine avant de calculer 3? Cela introduit une latence supplémentaire et cela prendra encore plus de temps que nécessaire avant que 4 ne soit disponible. En supposant qu’il y ait une latence de 1 cycle entre chaque cycle, 3 cycles seront nécessaires à partir de 1 jusqu’à ce que le résultat de 4 soit prêt et que nous puissions appeler la fonction.

Mais si nous les réorganisons et les évaluons dans l’ordre 1, 3, 2, 4, nous pouvons le faire en 2 cycles. 1 et 3 peuvent être démarrés dans le même cycle (ou même fusionnés en une seule instruction, puisque c’est la même expression), et dans ce qui suit, 2 et 4 peuvent être évalués. Tous les processeurs modernes peuvent exécuter 3-4 instructions par cycle, et un bon compilateur doit essayer de l’exploiter.

++ Kristo!

Le standard C ++ 1.9.16 prend tout son sens en ce qui concerne la façon dont on implémente operator ++ (postfix) pour une classe. Lorsque cette méthode operator ++ (int) est appelée, elle s’incrémente et renvoie une copie de la valeur d’origine. Exactement comme le dit la spécification C ++.

C’est bien de voir les normes s’améliorer!


Cependant, je me souviens très bien de l’utilisation d’anciens compilateurs C (pré-ANSI) dans lesquels:

 foo -> bar(i++) -> charlie(i++); 

N’a pas fait ce que vous en pensez! Au lieu de cela, il a compilé l’équivalent de:

 foo -> bar(i) -> charlie(i); ++i; ++i; 

Et ce comportement dépendait de la mise en œuvre du compilateur. (Rendre le portage amusant.)


Il est assez facile de tester et de vérifier que les compilateurs modernes se comportent maintenant correctement:

 #define SHOW(S,X) cout << S << ": " # X " = " << (X) << endl struct Foo { Foo & bar(const char * theString, int theI) { SHOW(theString, theI); return *this; } }; int main() { Foo f; int i = 0; f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i); SHOW("END ",i); } 


Répondre à un commentaire dans le fil de discussion ...

... Et en s'appuyant sur les réponses de TOUT LE MONDE ... (Merci les gars!)


Je pense que nous avons besoin d'écrire un peu mieux:

Donné:

 baz(g(),h()); 

Ensuite, nous ne soaps pas si g () sera invoqué avant ou après h () . C'est "non spécifié" .

Mais nous soaps que g () et h () seront invoqués avant baz () .

Donné:

 bar(i++,i++); 

Encore une fois, nous ne soaps pas quel i ++ sera évalué en premier, et peut-être même pas si je serai incrémenté une ou deux fois avant que bar () soit appelé. Les résultats sont indéfinis! (Étant donné que i = 0 , cela pourrait être une barre (0,0) ou une barre (1,0) ou une barre (0,1) ou quelque chose de vraiment bizarre!)


Donné:

 foo(i++); 

Nous soaps maintenant que je vais être incrémenté avant que foo () soit invoqué. Comme Kristo l’a souligné dans la section 1.9.16 du standard C ++:

Lors de l'appel d'une fonction (que la fonction soit ou non en ligne), chaque calcul de valeur et effet secondaire associé à toute expression d'argument ou à l'expression postfixée désignant la fonction appelée est séquencé avant l'exécution de chaque expression ou déclaration du corps appelé fonction. [Remarque: les calculs de valeur et les effets secondaires associés à différentes expressions d'argument ne sont pas séquencés. - note finale]

Bien que je pense que la section 5.2.6 le dit mieux:

La valeur d'une expression postfix ++ est la valeur de son opérande. [Note: la valeur obtenue est une copie de la valeur d'origine - note de fin] L'opérande doit être une lvalue modifiable. Le type de l'opérande doit être un type arithmétique ou un pointeur sur un type d'object effectif complet. La valeur de l’object opérande est modifiée en y ajoutant 1, sauf si l’object est de type bool, auquel cas il est défini sur true. [Note: cette utilisation est obsolète, voir Annexe D. - note de fin] Le calcul de la valeur de l'expression ++ est séquencé avant la modification de l'object opérande. En ce qui concerne un appel de fonction à séquence indéterminée, le fonctionnement de postfix ++ est une évaluation unique. [Note: Par conséquent, un appel de fonction ne doit pas intervenir entre la conversion lvalue-à-rvalue et l'effet secondaire associé à un seul opérateur postfix ++. - note finale] Le résultat est une valeur. Le type du résultat est la version non qualifiée cv du type de l'opérande. Voir aussi 5.7 et 5.17.

La norme, à la section 1.9.16, répertorie également (dans le cadre de ses exemples):

 i = 7, i++, i++; // i becomes 9 (valid) f(i = -1, i = -1); // the behavior is undefined 

Et nous pouvons le démontrer sortingvialement avec:

 #define SHOW(X) cout << # X " = " << (X) << endl int i = 0; /* Yes, it's global! */ void foo(int theI) { SHOW(theI); SHOW(i); } int main() { foo(i++); } 

Donc, oui, je suis incrémenté avant que foo () soit invoqué.


Tout cela a beaucoup de sens du sharepoint vue de:

 class Foo { public: Foo operator++(int) {...} /* Postfix variant */ } int main() { Foo f; delta( f++ ); } 

Ici, Foo :: operator ++ (int) doit être appelé avant delta () . Et l'opération d'incrémentation doit être terminée lors de cette invocation.


Dans mon exemple (peut-être trop complexe):

 f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i); 

f.bar ("A", i) doit être exécuté pour obtenir l'object utilisé pour object.bar ("B", i ++) , et ainsi de suite pour "C" et "D" .

Nous soaps donc que i ++ incrémente i avant d'appeler la barre ("B", i ++) (même si la barre ("B", ...) est appelée avec l'ancienne valeur de i ), et donc incrémentée avant la barre ( "C", i) et barre ("D", i) .


Revenir au commentaire de j_random_hacker :

j_random_hacker écrit:
+1, mais j'ai dû lire attentivement le standard pour me convaincre que cela allait. Ai-je raison de penser que si bar () était plutôt une fonction globale renvoyant say int, f était un int, et ces invocations étaient connectées par dire "^" au lieu de "." rapport "0"?

Cette question est beaucoup plus compliquée que vous ne le pensez ...

Réécrire votre question en code ...

 int bar(const char * theSsortingng, int theI) { SHOW(...); return i; } bar("A",i) ^ bar("B",i++) ^ bar("C",i) ^ bar("D",i); 

Maintenant, nous n'avons qu'une seule expression. Selon la norme (section 1.9, page 8, pdf page 20):

Note: les opérateurs ne peuvent être regroupés selon les règles mathématiques habituelles que si les opérateurs sont réellement associatifs ou commutatifs (7) Par exemple, dans le fragment suivant: a = a + 32760 + b + 5; l'expression expression se comporte exactement comme: a = (((a + 32760) + b) +5); en raison de l’associativité et de la préséance de ces opérateurs. Ainsi, le résultat de la sum (a + 32760) est ensuite ajouté à b, et ce résultat est ensuite ajouté à 5, ce qui donne la valeur affectée à a. Sur une machine dans laquelle les dépassements produisent une exception et dans lesquels la plage de valeurs pouvant être représentée par un int est [-32768, + 32767], l'implémentation ne peut pas réécrire cette expression sous la forme a = ((a + b) +32765); puisque si les valeurs pour a et b étaient respectivement -32754 et -15, la sum a + b produirait une exception alors que l'expression originale ne le ferait pas; l'expression ne peut pas non plus être réécrite comme a = ((a + 32765) + b); ou a = (a + (b + 32765)); comme les valeurs pour a et b peuvent avoir été, respectivement, 4 et -8 ou -17 et 12. Cependant, sur une machine dans laquelle les débordements ne génèrent pas d'exception et dans lesquels les résultats des débordements sont réversibles, l'instruction ci-dessus peut être réécrit par l'implémentation de l'une des manières ci-dessus, car le même résultat se produira. - note finale]

On pourrait donc penser que, en raison de la préséance, notre expression serait la même que:

 ( ( ( bar("A",i) ^ bar("B",i++) ) ^ bar("C",i) ) ^ bar("D",i) ); 

Mais, parce que (a ^ b) ^ c == a ^ (b ^ c) sans aucun débordement possible, il pourrait être réécrit dans n'importe quel ordre ...

Mais, étant donné que bar () est invoqué et pourrait impliquer des effets secondaires hypothétiques, cette expression ne peut pas être réécrite dans n'importe quel ordre. Les règles de préséance s'appliquent toujours.

Ce qui détermine bien l'ordre d'évaluation des barres () .

Maintenant, quand est-ce que i + = 1 se produit? Bien, il doit encore se produire avant que la barre ("B", ...) soit invoquée. (Même si la barre ("B", ....) est appelée avec l'ancienne valeur.)

Il se produit donc de manière déterministe avant la barre (C) et la barre (D) , et après la barre (A) .

Réponse: NON . Nous aurons toujours "A = 0, B = 0, C = 1, D = 1" si le compilateur est conforme aux normes.


Mais considérez un autre problème:

 i = 0; int & j = i; R = i ^ i++ ^ j; 

Quelle est la valeur de R?

Si le i + = 1 est survenu avant j , on aurait 0 ^ 0 ^ 1 = 1. Mais si le i + = 1 apparaissait après toute l'expression, nous aurions 0 ^ 0 ^ 0 = 0.

En fait, R est zéro. Le i + = 1 ne se produit qu'après l'évaluation de l'expression.


Ce que je pense est pourquoi:

i = 7, i ++, i ++; // je deviens 9 (valide)

Est légal ... Il a trois expressions:

  • i = 7
  • i ++
  • i ++

Et dans chaque cas, la valeur de i est modifiée à la fin de chaque expression. (Avant toute expression ultérieure est évaluée.)


PS: Considérez:

 int foo(int theI) { SHOW(theI); SHOW(i); return theI; } i = 0; int & j = i; R = i ^ i++ ^ foo(j); 

Dans ce cas, i + = 1 doit être évalué avant foo (j) . theI est 1. Et R est 0 ^ 0 ^ 1 = 1.

Pour développer la réponse de Bill the Lizard:

 int i = 1; foo(i++, i++); 

pourrait également entraîner un appel de fonction de

 foo(1, 1); 

(ce qui signifie que les réels sont évalués en parallèle, puis les postops sont appliqués).

– MarkusQ

Le Guru of the Week n ° 55 de Sutter (et la pièce correspondante dans “More Exceptional C ++”) traite de ce cas précis à titre d’exemple.

Selon lui, c’est du code parfaitement valide, et en fait un cas où l’on tente de transformer le relevé en deux lignes:

 items.erase (i);
 i ++

ne produit pas de code sémantiquement équivalent à l’instruction d’origine.