Ordre d'exécution C ++ dans le chaînage de méthodes

108

La sortie de ce programme:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Est:

method 1
method 2:0

Pourquoi nupas 1 quand meth2()commence?

Moises Viñas
la source
42
@MartinBonner: Bien que je connaisse la réponse, je ne l'appellerais pas «évidente» dans aucun sens du terme et, même si c'était le cas, ce ne serait pas une bonne raison de voter par contre. Décevant!
Courses de légèreté en orbite
4
C'est ce que vous obtenez lorsque vous modifiez vos arguments. Les fonctions modifiant leurs arguments sont plus difficiles à lire, leurs effets sont inattendus pour que le prochain programmeur travaille sur le code et elles conduisent à de telles surprises. Je suggère fortement d'éviter de modifier les paramètres à l'exception de l'invocant. La modification de l'appelant ne serait pas un problème ici, car la deuxième méthode est appelée sur le résultat de la première, donc les effets sont ordonnés dessus. Il y a encore des cas où ils ne le seraient pas.
Jan Hudec
@JanHudec C'est précisément pourquoi la programmation fonctionnelle met autant l'accent sur la pureté des fonctions.
Pharap
2
À titre d'exemple, une convention d' appel basée sur la pile serait probablement préférer pousser nu, &nuet csur la pile dans cet ordre, puis Invoke meth1, pousser le résultat sur la pile, puis Invoke meth2, alors qu'une convention d' appel fondé sur les registres voudrait charger cet &nudans les registres, invoquer meth1, charger nudans un registre, puis invoquer meth2.
Neil

Réponses:

66

Parce que l'ordre d'évaluation n'est pas spécifié.

Vous voyez nuen mainétant évalué 0avant même d' meth1être appelé. C'est le problème du chaînage. Je conseille de ne pas le faire.

Créez simplement un programme agréable, simple, clair, facile à lire et à comprendre:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Courses de légèreté en orbite
la source
14
Il est possible qu'une proposition visant à clarifier l'ordre d'évaluation dans certains cas , qui résout ce problème, soit présentée pour C ++ 17
Revolver_Ocelot
7
J'aime le chaînage de méthodes (par exemple <<pour la sortie, et les "constructeurs d'objets" pour les objets complexes avec trop d'arguments pour les constructeurs - mais cela se mélange vraiment mal avec les arguments de sortie.
Martin Bonner soutient Monica
34
Dois-je bien comprendre cela? l'ordre d'évaluation de meth1et meth2est défini, mais l'évaluation du paramètre pour meth2peut se produire avant meth1est appelée ...?
Roddy
7
Le chaînage de méthodes est correct tant que les méthodes sont sensées et ne modifient que l'appelant (pour lequel les effets sont bien ordonnés, car la deuxième méthode est appelée sur le résultat de la première).
Jan Hudec
4
C'est logique, quand on y pense. Cela fonctionne commemeth2(meth1(c, &nu), nu)
BartekChom
29

Je pense que cette partie du projet de norme concernant l'ordre d'évaluation est pertinente:

1.9 Exécution du programme

...

  1. 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, et qu'ils ne sont pas potentiellement simultanés, le comportement n'est pas défini

et aussi:

5.2.2 Appel de fonction

...

  1. [Remarque: les évaluations de l'expression postfixe et des arguments sont toutes non séquencées les unes par rapport aux autres. Tous les effets secondaires des évaluations d'arguments sont séquencés avant l'entrée de la fonction - note de fin]

Donc, pour votre ligne c.meth1(&nu).meth2(nu);, considérez ce qui se passe dans l'opérateur en termes d'opérateur d'appel de fonction pour l'appel final à meth2, afin que nous voyions clairement la ventilation dans l'expression et l'argument postfix nu:

operator()(c.meth1(&nu).meth2, nu);

Les évaluations de l'expression et de l'argument postfix pour l'appel de fonction final (c'est-à-dire l'expression postfixe c.meth1(&nu).meth2et nu) ne sont pas séquencées les unes par rapport aux autres selon la règle d' appel de fonction ci-dessus. Par conséquent, l' effet secondaire du calcul de l'expression de suffixe sur l'objet scalaire arn'est pas séquencé par rapport à l'évaluation de l'argument nuavant l' meth2appel de fonction. D'après la règle d' exécution du programme ci-dessus, il s'agit d'un comportement non défini.

En d'autres termes, il n'est pas nécessaire que le compilateur évalue l' nuargument de l' meth2appel après l' meth1appel - il est libre de supposer qu'aucun effet secondaire meth1n'affecte l' nuévaluation.

Le code d'assemblage produit par ce qui précède contient la séquence suivante dans la mainfonction:

  1. La variable nuest allouée sur la pile et initialisée avec 0.
  2. Un registre ( ebxdans mon cas) reçoit une copie de la valeur denu
  3. Les adresses de nuet csont chargées dans les registres de paramètres
  4. meth1 est appelé
  5. Le registre de valeur de retour et la valeur précédemment mise en cache de nudans le ebxregistre sont chargés dans les registres de paramètres
  6. meth2 est appelé

De manière critique, à l'étape 5 ci-dessus, le compilateur permet à la valeur mise en cache nude l'étape 2 d'être réutilisée dans l'appel de fonction à meth2. Ici, il ne tient pas compte de la possibilité qui nupeut avoir été modifiée par l'appel à meth1«comportement indéfini» en action.

REMARQUE: Cette réponse a changé en substance par rapport à sa forme originale. Mon explication initiale en termes d'effets secondaires du calcul des opérandes n'étant pas séquencée avant l'appel de fonction final était incorrecte, car ils le sont. Le problème est le fait que le calcul des opérandes eux-mêmes est séquencé de manière indéterminée.

Smeeheey
la source
2
C'est faux. Les appels de fonction sont séquencés de manière indéterminée avec d'autres évaluations dans la fonction appelante (sauf si une contrainte séquencée avant est imposée par ailleurs); ils ne s'entrelacent pas.
TC
1
@TC - Je n'ai jamais rien dit sur les appels de fonction entrelacés. Je n'ai évoqué que les effets secondaires des opérateurs. Si vous regardez le code d'assemblage produit par ce qui précède, vous verrez qu'il meth1est exécuté avant meth2, mais le paramètre pour meth2est une valeur de numis en cache dans un registre avant l'appel à meth1- c'est- à -dire que le compilateur a ignoré les effets secondaires potentiels, ce qui est conforme à ma réponse.
Smeeheey
1
Vous prétendez exactement que - "son effet secondaire (c'est-à-dire la définition de la valeur de ar) n'est pas garanti d'être séquencé avant l'appel". L'évaluation de l'expression postfixe dans un appel de fonction (qui est c.meth1(&nu).meth2) et l'évaluation de l'argument de cet appel ( nu) sont généralement non séquencées, mais 1) leurs effets secondaires sont tous séquencés avant l'entrée dans meth2et 2) puisqu'il c.meth1(&nu)s'agit d'un appel de fonction , il est séquencé de manière indéterminée avec l'évaluation de nu. À l'intérieur meth2, s'il obtenait en quelque sorte un pointeur vers la variable dans main, il verrait toujours 1.
TC
2
"Cependant, l'effet secondaire du calcul des opérandes (c'est-à-dire la définition de la valeur de ar) n'est pas garanti d'être séquencé avant quoi que ce soit (selon 2) ci-dessus)." Il est absolument garanti d'être séquencé avant l'appel à meth2, comme indiqué au point 3 de la page de référence que vous citez (que vous avez également négligé de citer correctement).
TC
1
Vous avez pris quelque chose de mal et vous l'avez aggravé. Il n'y a absolument aucun comportement indéfini ici. Continuez à lire [intro.execution] / 15, après l'exemple.
TC
9

Dans la norme C ++ de 1998, section 5, paragraphe 4

Sauf indication contraire, l'ordre d'évaluation des opérandes d'opérateurs individuels et des sous-expressions d'expressions individuelles, ainsi que l'ordre dans lequel les effets secondaires se produisent, n'est pas spécifié. Entre le point de séquence précédent et suivant, un objet scalaire verra sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. De plus, la valeur précédente ne sera accessible que pour déterminer la valeur à stocker. Les exigences de ce paragraphe doivent être remplies pour chaque ordre autorisé des sous-expressions d'une expression complète; sinon, le comportement n'est pas défini.

(J'ai omis une référence à la note de bas de page # 53 qui n'est pas pertinente pour cette question).

Essentiellement, &nudoit être évalué avant l'appel c1::meth1()et nudoit être évalué avant l'appel c1::meth2(). Il n'y a, cependant, aucune exigence nuà évaluer avant &nu(par exemple, il est permis d' nuêtre évalué en premier, puis &nu, puis c1::meth1()appelé - ce qui pourrait être ce que fait votre compilateur). Il n'est donc pas garanti que l' expression *ar = 1in c1::meth1()soit évaluée avant que nuin main()soit évaluée, afin d'être transmise à c1::meth2().

Les standards C ++ ultérieurs (que je n'ai pas actuellement sur le PC que j'utilise ce soir) ont essentiellement la même clause.

Peter
la source
7

Je pense que lors de la compilation, avant que les fonctions meth1 et meth2 ne soient vraiment appelées, les paramètres leur ont été passés. Je veux dire quand vous utilisez "c.meth1 (& nu) .meth2 (nu);" la valeur nu = 0 a été passée à meth2, donc peu importe que "nu" soit modifié par la suite.

vous pouvez essayer ceci:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

il obtiendra la réponse que vous voulez

T-shirt Saintor
la source