Renvoyer unique_ptr à partir des fonctions

367

unique_ptr<T>ne permet pas la construction de copie, mais prend en charge la sémantique de déplacement. Pourtant, je peux retourner un à unique_ptr<T>partir d'une fonction et affecter la valeur retourné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 compile et fonctionne comme prévu. Alors, comment se fait-il que cette ligne 1n'invoque pas le constructeur de copie et n'entraîne pas d'erreurs de compilation? Si je devais utiliser la ligne à la 2place, cela aurait du sens (l'utilisation de la ligne 2fonctionne également, mais nous ne sommes pas tenus de le faire).

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

Prétorien
la source
En théorie, si vous implémentiez une méthode d' usine , préférez-vous 1 ou 2 pour renvoyer la sortie de l'usine? Je présume que ce serait l'utilisation la plus courante de 1 car, avec une usine appropriée, vous voulez réellement que la propriété de la chose construite soit transférée à l'appelant.
Xharlie
7
@Xharlie? Ils passent tous les deux la propriété du unique_ptr. Toute la question porte sur 1 et 2 étant deux façons différentes de réaliser la même chose.
Prétorien du
dans ce cas, le RVO a également lieu en c ++ 0x, la destruction de l'objet unique_ptr se fera une fois, ce qui est effectué après la mainsortie de la fonction, mais pas au moment de la foosortie.
ampawd

Réponses:

219

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

Oui, voir 12.8 §34 et §35:

Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction copier / déplacer d'un objet classe [...] Cette élision des opérations de copie / déplacement, appelée copie élision , est autorisée [...] dans une déclaration de retour 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 type non qualifié cv que le 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 l, la résolution de surcharge pour sélectionner le constructeur de la copie est d'abord exécutée comme si l'objet était désigné par une valeur r .


Je voulais juste ajouter un point de plus que le retour par valeur devrait être le choix par défaut ici car une valeur nommée dans l'instruction de retour dans le pire des cas, c'est-à-dire sans élisions en C ++ 11, C ++ 14 et C ++ 17 est traitée comme valeur r. Ainsi, par exemple, la fonction suivante se compile avec le -fno-elide-constructorsdrapeau

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 l'indicateur est défini sur la compilation, deux mouvements (1 et 2) se produisent dans cette fonction, puis un mouvement plus tard (3).

fredoverflow
la source
@juanchopanza Voulez-vous dire essentiellement qu'il foo()est en effet également sur le point d'être détruit (s'il n'était affecté à rien), tout comme la valeur de retour dans la fonction, et qu'il est donc logique que C ++ utilise un constructeur de déplacement lors de l'exécution unique_ptr<int> p = foo();?
7cows
1
Cette réponse dit qu'une implémentation est autorisée à faire quelque chose ... elle ne dit pas qu'elle doit le faire, donc si c'était la seule section pertinente, cela impliquerait que s'appuyer sur ce comportement n'est pas portable. Mais je ne pense pas que ce soit vrai. Je suis enclin à penser que la bonne réponse a plus à voir avec le constructeur de mouvement, comme décrit dans la réponse de Nikola Smiljanic et Bartosz Milewski.
Don Hatch
6
@DonHatch Il dit qu'il est "autorisé" à effectuer l'élision de copie / déplacement dans ces cas, mais nous ne parlons pas d'élision de copie ici. C'est le deuxième paragraphe cité qui s'applique ici, qui s'appuie sur les règles d'élision de copie, mais n'est pas l'élision de copie elle-même. Il n'y a aucune incertitude dans le deuxième paragraphe - il est totalement portable.
Joseph Mansfield
@juanchopanza Je me rends compte que c'est maintenant 2 ans plus tard, mais pensez-vous toujours que c'est faux? Comme je l'ai mentionné dans le commentaire précédent, il ne s'agit pas de supprimer la copie. Il se trouve que dans les cas où la suppression de copie peut s'appliquer (même si elle ne peut pas s'appliquer avec std::unique_ptr), il existe une règle spéciale pour traiter d'abord les objets comme des valeurs r. Je pense que cela correspond entièrement à ce que Nikola a répondu.
Joseph Mansfield
1
Alors, pourquoi est-ce que j'obtiens toujours l'erreur "tentative de référence à une fonction supprimée" pour mon type de déplacement uniquement (constructeur de copie supprimé) lorsque je le renvoie exactement de la même manière que cet exemple?
DrumM
104

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

Si vous aviez une fonction qui accepte std::unique_ptrcomme argument, vous ne pourriez pas lui passer p. Vous devez invoquer explicitement 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;
}
Nikola Smiljanić
la source
3
@Fred - enfin, pas vraiment. Bien que ce pne soit pas temporaire, le résultat de foo()ce qui est retourné est; c'est donc une valeur r et elle peut être déplacée, ce qui rend l'affectation mainpossible. Je dirais que vous aviez tort, sauf que Nikola semble alors appliquer cette règle à plui-même qui EST par erreur.
Edward Strange
Exactement ce que je voulais dire, mais je n'ai pas trouvé les mots. J'ai supprimé cette partie de la réponse car elle n'était pas très claire.
Nikola Smiljanić
J'ai une question: dans la question initiale, y a-t-il une différence substantielle entre Line 1et Line 2? À mon avis, c'est la même chose car lors de la construction pdans main, il ne se soucie que du type de type de retour foo, non?
Hongxu Chen
1
@HongxuChen Dans cet exemple, il n'y a absolument aucune différence, voir la citation de la norme dans la réponse acceptée.
Nikola Smiljanić
En fait, vous pouvez utiliser p par la suite, tant que vous lui affectez. Jusque-là, vous ne pouvez pas essayer de référencer le contenu.
Alan
38

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 se liera qu'à une rvalue. C'est pourquoi vous obtenez une erreur lorsque vous essayez de passer une lvalue unique_ptr à une fonction. En revanche, une valeur renvoyée par une fonction est traitée comme une valeur r, de sorte que le constructeur de déplacement est appelé automatiquement.

Au fait, cela fonctionnera correctement:

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

Le temporaire unique_ptr ici est une rvalue.

Bartosz Milewski
la source
8
Je pense que le point est plus, pourquoi peut p- "évidemment" une valeur l - être traité comme une valeur r dans la déclaration de retour return p;dans la définition de foo. Je ne pense pas qu'il y ait de problème avec le fait que la valeur de retour de la fonction elle-même puisse être "déplacée".
CB Bailey
L'encapsulation de la valeur renvoyée par la fonction dans std :: move signifie-t-elle qu'elle sera déplacée deux fois?
3
@RodrigoSalazar std :: move est juste un cast de fantaisie d'une référence lvalue (&) à une référence rvalue (&&). L'utilisation étrangère de std :: move sur une référence rvalue sera simplement un noop
TiMoch
13

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

La partie de la norme bénissant le RVO continue en disant que si les conditions pour le RVO sont remplies, mais que les compilateurs choisissent de ne pas effectuer d'élision de copie, l'objet retourné doit être traité comme une valeur r. En effet, la norme exige que lorsque le RVO est autorisé, soit la suppression de la copie a lieu, soit elle std::moveest implicitement appliquée aux objets locaux renvoyés.

Ici, RVO fait référence à l' optimisation de la valeur de retour , et si les conditions pour le RVO sont remplies, cela signifie renvoyer l'objet local déclaré à l'intérieur de la fonction que vous attendez de faire le RVO , ce qui est également bien expliqué dans le point 25 de son livre en se référant à la norme (ici, l' objet local inclut les objets temporaires créés par l'instruction return). La plus grande conséquence de l'extrait est que la suppression de la copie a lieu ou std::moveest implicitement appliquée aux objets locaux renvoyés . Scott mentionne au point 25 questd::move est implicitement appliqué 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 pet le type de pest le même que le type de retour, ce qui entraîne la suppression de la copie. Et si le compilateur choisit de ne pas élider la copie, pour une raison quelconque, il se std::moveserait mis en ligne 1.

David Lee
la source
5

Une chose que je n'ai pas vue dans les autres réponses estPour clarifier une autre réponse , il y a une différence entre retourner std :: unique_ptr qui a été créé dans une fonction et celui qui a été donné à cette fonction.

L'exemple pourrait ressembler à 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));
v010dya
la source
Il est mentionné dans la réponse de fredoverflow - clairement mis en évidence " objet automatique ". Une référence (y compris une référence rvalue) n'est pas un objet automatique.
Toby Speight
@TobySpeight Ok, désolé. Je suppose que mon code n'est qu'une clarification alors.
v010dya