438 votes

Retourner un unique_ptr à partir de fonctions

unique_ptr<T> ne permet pas la construction de copies, mais supporte la sémantique du déplacement. Pourtant, je peux retourner un unique_ptr<T> d'une fonction et affecte 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 compile et fonctionne comme prévu. Alors comment se fait-il que la ligne 1 n'invoque pas le constructeur de copie et entraîne des erreurs de compilation ? Si je devais utiliser la ligne 2 au lieu de cela, il serait logique (en utilisant la ligne 2 fonctionne également, mais nous ne sommes pas tenus de le faire).

Je sais que C++0x permet cette exception pour unique_ptr puisque 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 retourné. Je suis curieux de savoir comment cela est mis en œuvre, est-ce un cas spécial dans le compilateur ou y a-t-il une autre clause dans la spécification du langage que cela exploite ?

0 votes

Hypothétiquement, si vous implémentez une usine préférez-vous 1 ou 2 pour retourner la sortie de l'usine ? Je présume que c'est l'utilisation la plus courante de 1 parce que, avec une bonne fabrique, vous voulez que la propriété de la chose construite soit transmise à l'appelant.

7 votes

@Xharlie ? Ils passent tous les deux la propriété de la unique_ptr . Toute la question porte sur le fait que 1 et 2 sont deux façons différentes de réaliser la même chose.

0 votes

Dans ce cas, la RVO a lieu en c++0x aussi, la destruction de l'objet unique_ptr sera une fois qui est effectuée après main sort, mais pas lorsque la fonction foo sortent.

259voto

FredOverflow Points 88201

Y a-t-il une autre clause dans la spécification du 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 des opérations de copie/déplacement d'un objet de classe [...]. Cette élision des opérations de copie/déplacement, appelée élision de la copie est autorisé [...] dans une déclaration de retour dans une fonction avec un type de retour de classe, lorsque l'expression est le nom d'un un objet automatique non volatile avec le même type cv-unqualified que le type de retour de la 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 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 r .


Je voulais juste ajouter un point supplémentaire : 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 dans C++11, C++14 et C++17, est traitée comme une rvalue. Ainsi, par exemple, la fonction suivante se compile avec l'attribut -fno-elide-constructors drapeau

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

Avec le drapeau sur la compilation, deux coups (1 et 2) se produisent dans cette fonction, puis un coup plus tard (3).

0 votes

@juanchopanza Voulez-vous dire essentiellement que foo() est en effet également sur le point d'être détruit (s'il n'a pas été affecté à quoi que ce soit), tout comme la valeur de retour de la fonction, et il est donc logique que le C++ utilise un constructeur de déplacement lors de l'exécution de la fonction unique_ptr<int> p = foo(); ?

1 votes

Cette réponse dit qu'une implémentation est autorisé de faire quelque chose... il n'est pas dit qu'il le doit, 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 le cas. Je suis enclin à penser que la réponse correcte a plus à voir avec le constructeur move, comme décrit dans la réponse de Nikola Smiljanic et Bartosz Milewski.

7 votes

@DonHatch Il est dit qu'il est "autorisé" d'effectuer une é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.

122voto

Nikola Smiljanić Points 15585

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

Si vous aviez une fonction qui accepte std::unique_ptr comme argument, vous ne pourriez pas lui passer p. Vous devriez invoquer explicitement le constructeur de mouvement, mais dans ce cas vous ne devriez 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;
}

3 votes

@Fred - Eh bien, pas vraiment. Bien que p n'est pas un temporaire, le résultat de foo() ce qui est renvoyé, l'est ; il s'agit donc d'une rvalue et elle peut être déplacée, ce qui rend l'affectation en main possible. Je dirais que vous avez tort, sauf que Nikola semble ensuite appliquer cette règle à p lui-même qui EST en erreur.

0 votes

Exactement ce que je voulais dire, mais je ne trouvais pas les mots. J'ai supprimé cette partie de la réponse car elle n'était pas très claire.

0 votes

J'ai une question : dans la question originale, y a-t-il une différence substantielle entre Line 1 et Ligne 2 ? A mon avis, c'est la même chose que lorsque l'on construit p en main il ne se préoccupe que du type de retour de l'utilisateur. foo n'est-ce pas ?

42voto

Bartosz Milewski Points 1739

Unique_ptr n'a pas le constructeur traditionnel de copie. 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 lie qu'à une rvalue. C'est pourquoi vous obtenez une erreur lorsque vous essayez de passer une lvalue unique_ptr à une fonction. D'un autre côté, une valeur qui est retournée par une fonction est traitée comme une rvalue, donc le constructeur move est appelé automatiquement.

D'ailleurs, cela fonctionnera correctement :

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

L'unique_ptr temporaire est ici une rvalue.

8 votes

Je pense que la question est plutôt de savoir pourquoi p - "évidemment" un lvalue - être traité comme un rvalue dans l'instruction 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".

0 votes

Le fait d'envelopper la valeur retournée par la fonction dans std::move signifie-t-il qu'elle sera déplacée deux fois ?

3 votes

@RodrigoSalazar std::move est juste un cast fantaisiste d'une référence lvalue (&) à une référence rvalue (&&). Une utilisation étrangère de std::move sur une référence rvalue sera simplement un noop.

20voto

David Lee Points 121

Je pense que c'est parfaitement expliqué dans point 25 de Scott Meyers C++ moderne et efficace . En voici un extrait :

La partie de la norme qui bénit le RVO poursuit en disant que si les conditions du RVO sont remplies, mais que les compilateurs choisissent de ne pas effectuer d'élision de copie, l'objet renvoyé doit être traité comme une rvalue. En fait, la norme exige que, lorsque le RVO est autorisé, soit l'élision de la copie a lieu, soit l'élision de la copie a lieu. std::move est implicitement appliquée aux objets locaux retournés.

Ici, RVO se réfère à optimisation de la valeur de retour y si les conditions de l'OAV sont remplies signifie qu'il faut renvoyer l'objet local déclaré à l'intérieur de la fonction dont on attend qu'elle fasse le RVO ce qui est également bien expliqué au point 25 de son livre en faisant référence à la norme (ici le objet local y compris les objets temporaires créés par l'instruction return). Ce qu'il faut retenir de cet extrait est le suivant soit l'élision de la copie a lieu, soit std::move est implicitement appliqué aux objets locaux retournés. . Scott mentionne au point 25 que std::move est appliqué implicitement lorsque le compilateur choisit de ne pas élider 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 le même que le type de retour, ce qui entraîne une élision de copie. Et si le compilateur choisit de ne pas élider la copie, pour une raison quelconque, std::move aurait donné un coup de pied dans la ligne 1 .

9voto

Volodya Points 382

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

L'exemple pourrait être le suivant :

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));

0 votes

Il est mentionné dans le réponse de fredoverflow - a clairement mis en évidence " automatique objet". Une référence (y compris une référence rvalue) n'est pas un objet automatique.

0 votes

@TobySpeight Ok, désolé. Je suppose que mon code est juste une clarification alors.

0 votes

Merci pour cette réponse ! Cela fait plusieurs jours que j'essaie de déboguer un problème causé par ce problème, et la lecture de cette réponse m'a permis de comprendre ce qui n'allait pas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X