web-dev-qa-db-fra.com

Moment exact du "retour" dans une fonction C ++

Cela semble être une question idiote, mais est-ce que le moment exact où return xxx; est "exécuté" dans une fonction définie sans ambiguïté?

S'il vous plaît voir l'exemple suivant pour voir ce que je veux dire ( ici vivre ):

#include <iostream>
#include <string>
#include <utility>

//changes the value of the underlying buffer
//when destructed
class Writer{
public:
    std::string &s;
    Writer(std::string &s_):s(s_){}
    ~Writer(){
        s+="B";
    }
};

std::string make_string_ok(){
    std::string res("A");
    Writer w(res);
    return res;
}


int main() {
    std::cout<<make_string_ok()<<std::endl;
} 

Ce à quoi je m'attends naïvement, alors que make_string_ok s'appelle:

  1. Le constructeur de res est appelé (la valeur de res est "A")
  2. Le constructeur de w est appelé
  3. return res est exécuté. La valeur actuelle de res doit être renvoyée (en copiant la valeur actuelle de res), c’est-à-dire "A".
  4. Le destructeur de w est appelé, la valeur de res devient "AB".
  5. Le destructeur de res est appelé.

Donc, je m'attendrais à "A" comme résultat, mais obtenez "AB" imprimé sur la console.

Par contre, pour une version légèrement différente de make_string:

std::string make_string_fail(){
    std::pair<std::string, int> res{"A",0};
    Writer w(res.first);
    return res.first;
}

le résultat est comme prévu - "A" ( voir live ).

La norme spécifie-t-elle quelle valeur doit être renvoyée dans les exemples ci-dessus ou est-elle indéterminée?

67
ead

Il s’agit de RVO (+ renvoyer une copie temporaire qui brouille l’image), une des optimisations permettant de modifier le comportement visible:

10.9.5 Copier/déplacer l'élision (les notes sont les miennes) :

Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction de copie/déplacement d'un objet de classe , même si le constructeur sélectionné pour la copie/déplacement l'opération et/ou le destructeur de l'objet ont des effets secondaires **. Dans de tels cas, l’implémentation traite la source et la cible de l’opération de copie/déplacement omise comme simplement deux façons différentes de faire référence au même objet .

Cette élision des opérations de copie/déplacement, appelée élision de copie, est autorisée dans les cas suivants (qui peuvent être combinés pour éliminer plusieurs copies):

  • dans une instruction return dans une fonction avec un type de retour de classe, lorsque l'expression est le nom d'un objet automatique non volatile (autre qu'un paramètre de fonction) ou une variable introduite par la déclaration d'exception d'un gestionnaire) avec le même type (en ignorant cv-qualification) que le type de retour de la fonction , la copie/l'opération de déplacement peut être omise en construisant l'objet automatique directement dans l'objet de retour de l'appel de fonction
  • [...]

Selon que cela s'applique ou non, votre prémisse se trompe. En 1. le c'tor pour res est appelé, mais l'objet peut vivre à l'intérieur de make_string_ok ou à l'extérieur.

Cas 1.

Les balles 2. et 3. pourraient ne pas se produire du tout, mais il s’agit d’un point secondaire. La cible a eu des effets secondaires de Writersur affecté, était en dehors de make_string_ok. Il s’agit d’un temporaire créé à l’aide de make_string_ok dans le contexte de l’évaluation operator<<(ostream, std::string). Le compilateur a créé une valeur temporaire, puis a exécuté la fonction. Ceci est important car les vies temporaires sont en dehors de celle-ci, donc la cible de Writer n’est pas locale à make_string_ok mais à operator<<.

Cas 2.

En attendant, votre deuxième exemple ne correspond pas au critère (ni à ceux qui ont été omis pour des raisons de brièveté) car les types sont différents. Alors l'écrivain meurt. Il mourrait même s'il faisait partie de la pair. Donc, ici, une copie de res.first est renvoyée en tant qu'objet temporaire, puis le rôle de Writer affecte l'original res.first, qui est sur le point de mourir.

Il semble assez évident que la copie est faite avant d'appeler les destructeurs, car l'objet renvoyé par copie est également détruit. Vous ne pourrez donc pas le copier autrement.

Après tout, cela se résume à RVO, parce que l’or de Writer fonctionne soit sur l’objet extérieur, soit sur l’objet local, selon que l’optimisation est appliquée ou non.

La norme spécifie-t-elle quelle valeur doit être renvoyée dans les exemples ci-dessus ou est-ce non spécifiée?

Non, l'optimisation est facultative, bien qu'elle puisse modifier le comportement observable. C'est à la discrétion du compilateur de l'appliquer ou non. C'est une exception à la règle du "général si-si" qui dit que le compilateur est autorisé à effectuer toute transformation qui ne change pas le comportement observable.

Un cas pour cela est devenu obligatoire en c ++ 17, mais pas le vôtre. Le obligatoire est où la valeur de retour est un temporaire non nommé.

28
luk32

En raison de optimisation de la valeur de retour (RVO) , un destructeur pour std::string res dans make_string_ok ne peut pas être appelé. L'objet string peut être construit du côté de l'appelant et la fonction ne peut initialiser que la valeur.

Le code sera équivalent à:

void make_string_ok(std::string& res){
    Writer w(res);
}

int main() {
    std::string res("A");
    make_string_ok(res);
}

C'est pourquoi la valeur de retour doit être "AB".

Dans le deuxième exemple, RVO ne s'applique pas et la valeur sera copiée dans la valeur renvoyée exactement lors de l'appel à retourner, et le destructeur de Writer s'exécutera sur res.first après la copie.

6.6 Déclarations de saut

À la sortie d'une portée (quelle que soit sa réalisation), des destructeurs (12.4) sont appelés pour tous les objets construits à durée de stockage automatique (3.7.2) (objets nommés ou temporaires) déclarés dans cette portée, dans l'ordre inverse de leur déclaration. Transférer hors d'une boucle, d'un bloc ou en arrière d'une variable initialisée à durée de stockage automatique implique la destruction de variables à durée de stockage automatique qui se trouvent dans la portée au point transféré de ...

...

6.6.3 La déclaration de retour

L’initialisation de la copie de l’entité renvoyée est séquencée avant la destruction des éléments temporaires, à la fin de l’expression complète établie par l’opérande de l’instruction return, elle-même séquencée avant la destruction des variables locales (6.6) du paramètre. bloquer la déclaration de retour.

...

12.8 Copier et déplacer des objets de classe

31 Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction copie/déplacement d'un objet de classe, même si le constructeur et/ou le destructeur copie/déplacement de l'objet ont des effets secondaires. Dans de tels cas, l’implémentation traite la source et la cible de l’opération de copie/déplacement omise comme simplement deux manières différentes de faire référence au même objet, et la destruction de cet objet se produit au plus tard des moments où les deux objets auraient été détruit sans optimisation. (123) Cette élision des opérations de copie/déplacement, appelée copie de la copie, est autorisée dans les cas suivants (pouvant être combinés pour éliminer plusieurs copies):

- dans une instruction return dans une fonction avec un type de retour de classe, lorsque l'expression est le nom d'un objet automatique non volatile (autre qu'un paramètre function ou catch-clause) avec le même type cvunqualified que le type de retour de la fonction, l'opération de copie/déplacement peut être omise en construisant l'objet automatique directement dans la valeur de retour de la fonction

123) Comme un seul objet est détruit au lieu de deux, et qu'un constructeur de copie/déplacement n'est pas exécuté, il reste un objet détruit pour chaque objet construit.

36
Shloim

Il existe un concept en C++ appelé elision.

Elision prend deux objets apparemment distincts et fusionne leur identité et leur durée de vie.

Avant c ++ 17 , une élision pourrait se produire:

  1. Lorsque vous avez une variable non-paramètre Foo f; dans une fonction qui renvoie Foo et que l'instruction return est un simple return f;.

  2. Lorsque vous utilisez un objet anonyme pour construire à peu près tout autre objet.

Dans c ++ 17 tous les cas (presque?) De # 2 sont éliminés par les nouvelles règles de valeur; elision n’a plus lieu, car ce qui était utilisé pour créer un objet temporaire ne le fait plus. Au lieu de cela, la construction du "temporaire" est directement liée à l'emplacement de l'objet permanent.

Or, l’élision n’est pas toujours possible étant donné l’ABI qu’un compilateur compile. Il existe deux cas courants, l’optimisation de la valeur de retour et l’optimisation de la valeur de retour nommée.

RVO est le cas comme ceci:

Foo func() {
  return Foo(7);
}
Foo foo = func();

où nous avons une valeur de retour Foo(7) qui est éliée dans la valeur renvoyée, qui est ensuite éliée dans la variable externe foo. Ce qui semble être 3 objets (la valeur de retour de foo(), la valeur sur la ligne return et Foo foo) est en réalité égale à 1 au moment de l'exécution.

Avant c ++ 17 , les constructeurs copier/déplacer doivent exister ici et l’élision est facultative; in c ++ 17 En raison des nouvelles règles de valeur, aucune construction/modification de construction n’existe, et il n’existe aucune option pour le compilateur, il doit y avoir 1 valeur ici.

L'autre cas célèbre s'appelle l'optimisation de la valeur de retour, NRVO. C'est le (1) cas d'élision ci-dessus.

Foo func() {
  Foo local;
  return local;
}
Foo foo = func();

encore une fois, elision peut fusionner la durée de vie et l’identité de Foo local, la valeur de retour de func et Foo foo en dehors de func.

Même c ++ 17 , la deuxième fusion (entre la valeur de retour de func et Foo foo) n'est pas facultative (et techniquement, la valeur renvoyée par func n'est jamais un objet, juste une expression, qui est ensuite lié à la construction de Foo foo), mais le premier reste facultatif et nécessite l'existence d'un constructeur de déplacement ou de copie.

L’élision est une règle qui peut survenir même si l’élimination de ces copies, destructions et constructions aurait des effets secondaires observables; ce n'est pas une optimisation "comme si". Au lieu de cela, il s'agit d'un changement subtil par rapport à ce qu'une personne naïve pourrait penser du code C++. L'appeler une "optimisation" est plus qu'un abus de langage.

Le fait que ce soit optionnel et que des choses subtiles puissent le casser pose problème.

Foo func(bool b) {
  Foo long_lived;
  long_lived.futz();
  if (b)
  {
    Foo short_lived;
    return short_lived;
  }
  return long_lived;
}

dans le cas ci-dessus, bien qu'il soit légal pour un compilateur d'élider à la fois Foo long_lived et Foo short_lived, les problèmes d'implémentation la rendent fondamentalement impossible, car les deux objets ne peuvent pas avoir leur durée de vie fusionnée avec la valeur de retour de func; eliding short_lived et long_lived ensemble n'est pas légal, et leurs durées de vie se chevauchent.

Vous pouvez toujours le faire sous la forme, mais seulement si vous pouvez examiner et comprendre tous les effets secondaires des destructeurs, des constructeurs et de .futz().

16