web-dev-qa-db-fra.com

Renvoyer unique_ptr à partir de fonctions

unique_ptr<T> n'autorise pas la construction de copie, mais prend en charge la sémantique de déplacement. Cependant, je peux renvoyer un unique_ptr<T> d'une fonction et affecter la valeur renvoyée à une variable.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

Le code ci-dessus est compilé et fonctionne comme prévu. Alors, comment se fait-il que la ligne 1 n'appelle pas le constructeur de copie et ne génère des erreurs de compilation? Si je devais utiliser la ligne 2 à la place, cela aurait du sens (utiliser la ligne 2 fonctionne également, mais nous ne sommes pas obligés de le faire).

Je sais que C++ 0x autorise cette exception à unique_ptr puisque la valeur de retour est un objet temporaire qui sera détruit dès la sortie de la fonction, garantissant ainsi le caractère unique du pointeur renvoyé. Je suis curieux de savoir comment cela est implémenté, est-ce spécial dans le compilateur ou existe-t-il une autre clause dans la spécification de langage que cela exploite?

316
Praetorian

y a-t-il une autre clause dans la spécification de langage que celle-ci exploite?

Oui, voir 12.8 §34 et §35:

Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction copie/déplacement d'un objet de classe [...] Cette élision d'opérations de copie/déplacement, appelée copy elision , est autorisé 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 avec le même cv type non qualifié comme type de retour de fonction [...]

Lorsque les critères d'élision d'une opération de copie sont remplis et que l'objet à copier est désigné par une valeur lvalue, la résolution de surcharge pour sélectionner le constructeur de la copie est d'abord effectuée comme si l'objet était désigné par une valeur rvalue =.


Je voulais juste ajouter un autre point: renvoyer par valeur devrait être le choix par défaut ici car une valeur nommée dans l'instruction return dans le pire des cas, c'est-à-dire sans élisions en C++ 11, C++ 14 et C++ 17, est traitée comme une valeur. Ainsi, par exemple, la fonction suivante est compilée avec le drapeau -fno-elide-constructors

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

Lorsque le drapeau est activé lors de la compilation, deux mouvements (1 et 2) sont exécutés dans cette fonction, puis un mouvement plus tard (3).

199
fredoverflow

Ceci n'est en aucun cas spécifique à std::unique_ptr, mais s'applique à toute classe pouvant être déplacée. C'est garanti par les règles de langue puisque vous retournez par valeur. Le compilateur essaie d'éluder les copies, appelle un constructeur de déplacement s'il ne peut pas en supprimer, appelle un constructeur de copie s'il ne peut pas se déplacer et ne parvient pas à compiler s'il ne peut pas copier.

Si vous avez une fonction qui accepte std::unique_ptr comme argument, vous ne pourrez pas lui passer p. Vous devez explicitement appeler le constructeur de déplacement, mais dans ce cas, vous ne devez pas utiliser la variable p après l'appel à bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}
95
Nikola Smiljanić

unique_ptr n'a pas le constructeur de copie traditionnel. Au lieu de cela, il a un "constructeur de déplacement" qui utilise des références rvalue:

unique_ptr::unique_ptr(unique_ptr && src);

Une référence rvalue (la double esperluette) ne sera liée qu’à une valeur rvalue. C'est pourquoi vous obtenez une erreur lorsque vous essayez de transmettre une lvalue unique_ptr à une fonction. Par ailleurs, une valeur renvoyée par une fonction est traitée comme une valeur rvalue. Le constructeur du déplacement est donc appelé automatiquement.

Au fait, cela fonctionnera correctement:

bar(unique_ptr<int>(new int(44));

Le temporaire unique_ptr est une valeur rvalue.

37
Bartosz Milewski

Je pense que cela est parfaitement expliqué dans le point 25 de Scott Meyers ' Effective Modern C++ . Voici un extrait:

La partie de la norme qui bénit RVO continue en disant que si les conditions pour le RVO sont remplies, mais que les compilateurs choisissent de ne pas effectuer la copie, l'objet retourné doit être traité comme une valeur. En effet, la norme exige que, lorsque le paramètre RVO est autorisé, soit une copie soit effectuée, soit std::move soit appliqué implicitement aux objets locaux renvoyés.

Ici, RVO fait référence à optimisation de la valeur de retour , et si les conditions pour le RVO sont remplies signifie que l'objet local déclaré dans la fonction doit être renvoyé RVO, qui est également bien expliqué au point 25 de son livre en faisant référence à la norme (ici l'objet local inclut les objets temporaires créés par l'instruction return). Le plus gros inconvénient de l'extrait est , soit une copie de la copie a lieu, soit std::move est appliqué implicitement aux objets locaux renvoyés . Scott mentionne au point 25 que std::move est appliqué implicitement lorsque le compilateur choisit de ne pas supprimer la copie et que le programmeur ne doit pas le faire explicitement.

Dans votre cas, le code est clairement un candidat pour RVO car il renvoie l'objet local p et le type de p est identique au type de retour, ce qui entraîne la suppression de la copie. Et si le compilateur choisit de ne pas supprimer la copie, pour quelque raison que ce soit, std::move serait entré dans la ligne 1.

10
David Lee

Une chose que je n'ai pas vue dans d'autres réponses est Pour clarifier ne autre réponse il y a une différence entre le retour de std :: unique_ptr créé dans une fonction et celui attribué à cette fonction.

L'exemple pourrait être comme ceci:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
2
v010dya