web-dev-qa-db-fra.com

Comment passer un argument unique_ptr à un constructeur ou à une fonction?

Je suis nouveau pour déplacer la sémantique dans C++ 11 et je ne sais pas très bien comment gérer les paramètres unique_ptr dans les constructeurs ou les fonctions. Considérons cette classe se référant elle-même:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

Est-ce que c'est comme ça que je devrais écrire des fonctions en prenant unique_ptr arguments?

Et dois-je utiliser std::move dans le code de l'appelant?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
370
codablank1

Voici les manières possibles de prendre un pointeur unique comme argument, ainsi que la signification qui leur est associée.

(A) par valeur

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Pour que l'utilisateur appelle ceci, il doit effectuer l'une des opérations suivantes:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Prendre un pointeur unique par valeur signifie que vous êtes en train de transférer la propriété du pointeur à la fonction/objet/etc en question. Une fois que newBase est construit, il est garanti que nextBase sera vide . Vous ne possédez pas l'objet et vous n'avez même plus de pointeur. C'est parti.

Ceci est assuré parce que nous prenons le paramètre par valeur. std::move ne bouge pas ; c'est juste un casting de fantaisie. std::move(nextBase) renvoie un Base&& qui est une référence de valeur r à nextBase. C'est tout ce que ça fait.

Parce que Base::Base(std::unique_ptr<Base> n) prend son argument par valeur plutôt que par référence à r-valeur, C++ construira automatiquement un temporaire pour nous. Il crée un std::unique_ptr<Base> à partir du Base&& que nous avons donné à la fonction via std::move(nextBase). C’est la construction de ce temporaire qui fait que déplace la valeur de nextBase dans l’argument de la fonction n.

(B) Par référence de valeur l non constante

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

Cela doit être appelé sur une valeur l réelle (une variable nommée). Il ne peut pas être appelé avec un temporaire comme celui-ci:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

La signification de ceci est la même que la signification de toute autre utilisation de références non const: la fonction peut ou non revendiquer la propriété du pointeur. Étant donné ce code:

Base newBase(nextBase);

Il n'y a aucune garantie que nextBase soit vide. Il peut être vide; il se peut que non. Cela dépend vraiment de ce que Base::Base(std::unique_ptr<Base> &n) veut faire. À cause de cela, il n’est pas très évident de voir ce qui va se passer dans la signature de la fonction; vous devez lire l'implémentation (ou la documentation associée).

Pour cette raison, je ne suggérerais pas cela comme interface.

(C) Par référence constante de valeur l

Base(std::unique_ptr<Base> const &n);

Je ne montre pas d'implémentation, car vous ne pouvez pas passer d'un const&. En passant un const&, vous dites que la fonction peut accéder à la Base via le pointeur, mais elle ne peut pas le stocker nulle part. Il ne peut pas en revendiquer la propriété.

Cela peut être utile. Pas nécessairement pour votre cas particulier, mais il est toujours bon de pouvoir remettre un pointeur à quelqu'un et de savoir qu'il ne peut (sans enfreindre les règles de C++, comme pas de rejet const) en revendiquer la propriété. Ils ne peuvent pas le stocker. Ils peuvent le transmettre à d'autres, mais ces derniers doivent respecter les mêmes règles.

(D) Par référence de valeur r

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Ceci est plus ou moins identique au cas "par référence non constante de valeur l". Les différences sont deux choses.

  1. Vous pouvez passer un temporaire:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
    
  2. Vous devez utiliser std::move lorsque vous transmettez des arguments non temporaires.

Ce dernier est vraiment le problème. Si vous voyez cette ligne:

Base newBase(std::move(nextBase));

Vous vous attendez raisonnablement à ce que, après la fin de cette ligne, nextBase soit vide. Il aurait dû être déplacé de. Après tout, vous avez ce std::move assis à cet endroit, vous indiquant que des mouvements se sont produits.

Le problème est que ce n'est pas le cas. Il n'est pas garanti d'avoir été déplacé de. Il peut avoir été déplacé de, mais vous le saurez uniquement en consultant le code source. Vous ne pouvez pas dire seulement à partir de la signature de la fonction.

Recommandations

  • (A) par valeur: Si vous entendez par une fonction réclamer la propriété d'un unique_ptr, prenez-le par valeur.
  • (C) Par la référence constante de la valeur l: Si vous voulez qu'une fonction utilise simplement le unique_ptr pendant la durée de son exécution, prenez-le par const&. Vous pouvez également passer un & ou const& au type actuel pointé, plutôt que d'utiliser un unique_ptr.
  • (D) Par référence de valeur r: Si une fonction peut ou non revendiquer la propriété (en fonction des chemins de code internes), prenez-la par &&. Mais je déconseille fortement de faire cela autant que possible.

Comment manipuler unique_ptr

Vous ne pouvez pas copier un unique_ptr. Vous pouvez seulement le déplacer. Pour ce faire, utilisez la fonction de bibliothèque standard std::move.

Si vous prenez un unique_ptr par valeur, vous pouvez vous en déplacer librement. Mais le mouvement ne se produit pas réellement à cause de std::move. Prenez la déclaration suivante:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Ce sont vraiment deux déclarations:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(Remarque: le code ci-dessus ne compile pas techniquement, car les références de valeur r non temporaires ne sont pas réellement des valeurs r. Il est ici uniquement à des fins de démonstration).

La temporary n'est qu'une référence de valeur r à oldPtr. C'est dans le constructeur de newPtr où le mouvement se produit. Le constructeur de mouvement de unique_ptr (un constructeur qui prend un && sur lui-même) est ce que fait le mouvement réel.

Si vous avez une valeur unique_ptr et que vous voulez la stocker quelque part, vous devez utiliser std::move pour effectuer le stockage.

782
Nicol Bolas

Laissez-moi essayer d’énoncer les différents modes viables de transfert de pointeurs vers des objets dont la mémoire est gérée par une instance du modèle de classe std::unique_ptr; cela s'applique également au modèle de classe std::auto_ptr plus ancien (qui, je crois, autorise toutes les utilisations de ce pointeur unique, mais pour lequel des valeurs modifiables seront également acceptées là où des valeurs sont attendues, sans avoir à invoquer std::move ), et dans une certaine mesure également à std::shared_ptr.

Comme exemple concret pour la discussion, je considérerai le type de liste simple suivant

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

Les instances de cette liste (qui ne peuvent pas partager des parties avec d'autres instances ou être circulaires) appartiennent entièrement à celui qui détient le pointeur initial list. Si le code client sait que la liste qu'il stocke ne sera jamais vide, il peut également choisir de stocker directement le premier node plutôt qu'un list. Aucun destructeur pour node ne doit être défini: les destructeurs de ses champs étant appelés automatiquement, la liste entière sera supprimée de manière récursive par le destructeur du pointeur intelligent une fois que la durée de vie du pointeur ou du noeud initial sera terminée.

Ce type récursif offre l’occasion de discuter de cas moins visibles dans le cas d’un pointeur intelligent sur des données simples. De plus, les fonctions elles-mêmes fournissent parfois (de manière récursive) un exemple de code client. Le typedef pour list est bien sûr biaisé en faveur de unique_ptr, mais la définition pourrait être modifiée pour utiliser auto_ptr ou shared_ptr sans trop besoin de changer pour ce qui est dit ci-dessous ( notamment en ce qui concerne la sécurité des exceptions, sans qu'il soit nécessaire d’écrire des destructeurs).

Modes de transmission des pointeurs intelligents

Mode 0: passe un pointeur ou un argument de référence au lieu d'un pointeur intelligent

Si votre fonction n'est pas concernée par la propriété, c'est la méthode préférée: ne lui faites pas du tout prendre un pointeur intelligent. Dans ce cas, votre fonction n'a pas besoin de s'inquiéter de qui est propriétaire de l'objet pointé, ou par quel moyen la propriété est gérée, donc passer un pointeur brut est à la fois parfaitement sûr et sûr. forme la plus flexible, puisqu'un client peut toujours produire un pointeur brut (indépendamment de la propriété) (en appelant la méthode get ou à partir de l'opérateur adresse-de &).

Par exemple, la fonction permettant de calculer la longueur d'une telle liste ne devrait pas être un argument list, mais un pointeur brut:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

Un client qui détient une variable list head peut appeler cette fonction sous le nom length(head.get()), tandis qu'un client qui a choisi de stocker un node n représentant une liste non vide peut appeler length(&n). .

Si le pointeur a la garantie d'être non nul (ce qui n'est pas le cas ici puisque les listes peuvent être vides), on peut préférer passer une référence plutôt qu'un pointeur. Il peut s'agir d'un pointeur/d'une référence à non -const si la fonction doit mettre à jour le contenu du ou des nœuds, sans en ajouter ni en supprimer (ce dernier impliquerait la propriété).

Un cas intéressant qui entre dans la catégorie mode 0 est en train de faire une copie (complète) de la liste; bien qu'une fonction faisant cela doive bien sûr transférer la propriété de la copie créée, elle ne concerne pas la propriété de la liste qu'elle copie. Donc, cela pourrait être défini comme suit:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

Ce code mérite une analyse approfondie, tant pour la question de savoir pourquoi il compile (le résultat de l'appel récursif de copy dans la liste d'initialisation se lie à l'argument de référence rvalue dans le constructeur de déplacement de unique_ptr<node>, aka list, lors de l'initialisation du champ next du node généré, et de la question de savoir pourquoi il est protégé contre les exceptions (si pendant le processus d'allocation récursive, la mémoire s'épuise et un appel de new jette std::bad_alloc, puis à ce moment-là un pointeur sur la liste partiellement construite est tenu anonymement dans un fichier temporaire de type list créé pour la liste d'initialiseurs, et son destructeur nettoyer cette liste partielle). Soit dit en passant, il convient de résister à la tentation de remplacer (comme je l’ai fait au début) le second nullptr par p qui, après tout, est réputé nul à ce stade: il est impossible de construire un pointeur intelligent à partir d’un (raw) pointeur sur constant , même s'il est connu pour être nul.

Mode 1: passe un pointeur intelligent par valeur

Une fonction qui prend une valeur de pointeur intelligent en tant qu'argument prend possession de l'objet pointé immédiatement: le pointeur intelligent détenu par l'appelant (que ce soit dans une variable nommée ou temporaire temporaire) est copié dans la valeur d'argument à l'entrée de la fonction et l'appelant le pointeur est devenu nul (dans le cas d'une copie temporaire, la copie peut avoir été supprimée, mais dans tous les cas l'appelant a perdu l'accès à l'objet pointé). Je voudrais appeler ce mode appel en espèces : l'appelant paie à l'avance pour le service appelé et ne peut se faire aucune illusion quant à la propriété après l'appel. Pour que cela soit clair, les règles de langage exigent que l'appelant encapsule l'argument dans std::move si le pointeur intelligent est maintenu dans une variable (techniquement, si l'argument est une lvalue); dans ce cas (mais pas pour le mode 3 ci-dessous), cette fonction fait ce que son nom suggère, à savoir déplacer la valeur de la variable vers une variable temporaire, en laissant la variable null.

Dans les cas où la fonction appelée acquiert sans réserve la propriété de (l'objet Pilfers) indiqué, ce mode utilisé avec std::unique_ptr ou std::auto_ptr constitue un bon moyen de passer un pointeur avec sa propriété, ce qui évite toute risque de fuite de mémoire. Néanmoins, je pense qu'il n'y a que très peu de situations dans lesquelles le mode 3 ci-dessous ne doit pas être préféré (même si légèrement) au mode 1. Pour cette raison, je ne fournirai aucun exemple d'utilisation de ce mode. (Mais voir l'exemple reversed du mode 3 ci-dessous, où il est remarqué que le mode 1 ferait au moins aussi bien.) Si la fonction prend plus d'arguments que simplement ce pointeur, il peut arriver qu'il y ait aussi raison technique pour éviter le mode 1 (avec std::unique_ptr ou std::auto_ptr): une opération de déplacement réelle ayant lieu lors du passage d'un pointeur variable p par l'expression std::move(p), on ne peut pas supposer que p détient une valeur utile lors de l'évaluation des autres arguments (l'ordre d'évaluation étant non spécifié), ce qui peut entraîner des erreurs subtiles; En revanche, l'utilisation du mode 3 garantit qu'aucun mouvement de p n'a lieu avant l'appel de la fonction. Par conséquent, les autres arguments peuvent accéder en toute sécurité à une valeur via p.

Utilisé avec std::shared_ptr, ce mode est intéressant car, avec une définition de fonction unique, il permet à l'appelant de choisir de conserver ou non une copie de partage du pointeur pendant créer une nouvelle copie de partage à utiliser par la fonction (cela se produit lorsqu'un argument lvalue est fourni; le constructeur de la copie pour les pointeurs partagés utilisés lors de l'appel augmente le nombre de références), ou simplement pour donner à la fonction une copie du pointeur sans la conserver un ou touchant le compte de références (cela se produit lorsqu'un argument rvalue est fourni, éventuellement une valeur lvalue encapsulée dans un appel de std::move). Par exemple

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

La même chose pourrait être obtenue en définissant séparément void f(const std::shared_ptr<X>& x) (pour le cas lvalue) et void f(std::shared_ptr<X>&& x) (pour le cas rvalue), les corps des fonctions ne différant que par le fait que la première version invoque la sémantique de la copie (en utilisant la construction de copie/affectation lors de l'utilisation de x), mais la deuxième version déplace la sémantique (écriture à la place de std::move(x), comme dans l'exemple de code). Donc, pour les pointeurs partagés, le mode 1 peut être utile pour éviter la duplication de code.

Mode 2: passe un pointeur intelligent par référence (modifiable) lvalue

Ici, la fonction nécessite simplement d'avoir une référence modifiable au pointeur intelligent, mais ne donne aucune indication sur ce qu'elle en fera. Je souhaite appeler cette méthode , appel par carte : le demandeur assure le paiement en indiquant un numéro de carte de crédit. La référence peut être utilisée pour s'approprier l'objet pointé, mais ce n'est pas obligatoire. Ce mode nécessite de fournir un argument modifiable de lvalue, correspondant au fait que l’effet souhaité de la fonction peut inclure de laisser une valeur utile dans la variable d’argument. Un appelant avec une expression rvalue qu'il souhaite transmettre à une telle fonction serait obligé de la stocker dans une variable nommée pour pouvoir effectuer l'appel, car le langage ne fournit qu'une conversion implicite en une constante . Référence de la valeur (se référant à un temporaire) à partir d'une valeur. (Contrairement à la situation opposée gérée par std::move, une conversion de Y&& en Y&, avec Y le type de pointeur intelligent, n'est pas possible; néanmoins, cette conversion peut être obtenue par une fonction de modèle simple si vous le souhaitez vraiment, voir https://stackoverflow.com/a/24868376/1436796 ). Pour le cas où la fonction appelée a l'intention de s'approprier inconditionnellement l'objet, dérogeant à l'argument, l'obligation de fournir un argument lvalue donne le mauvais signal: la variable n'aura aucune valeur utile après l'appel. Par conséquent, le mode 3, qui donne des possibilités identiques dans notre fonction mais demande aux appelants de fournir une valeur, doit être préféré pour un tel usage.

Cependant, il existe un cas d’utilisation valide pour le mode 2, à savoir des fonctions qui peuvent modifier le pointeur ou l’objet pointé vers d’une manière impliquant la propriété . Par exemple, une fonction qui préfixe un nœud à un list fournit un exemple d'utilisation de ce type:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

Clairement, il ne serait pas souhaitable ici de forcer les appelants à utiliser std::move, car leur pointeur intelligent possède toujours une liste bien définie et non vide après l'appel, bien qu'elle soit différente de la précédente.

Là encore, il est intéressant d’observer ce qui se passe si l’appel prepend échoue faute de mémoire disponible. Ensuite, l'appel new lève std::bad_alloc; à ce moment précis, aucune node ne pouvant être attribuée, il est certain que la référence rvalue passée (mode 3) de std::move(l) ne peut pas encore avoir été volée, car cela serait fait pour construire le next champ de la node qui n'a pas pu être alloué. Ainsi, le pointeur intelligent d'origine l conserve la liste d'origine lorsque l'erreur est générée. cette liste sera soit détruite correctement par le destructeur de pointeur intelligent, soit au cas où l devrait survivre grâce à une clause catch suffisamment précoce, il conservera toujours la liste d'origine.

C'était un exemple constructif. avec un clin d'oeil à cette question on peut aussi donner l'exemple le plus destructeur de supprimer le premier nœud contenant une valeur donnée, le cas échéant:

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

Encore une fois, la correction est assez subtile ici. Notamment, dans la déclaration finale, le pointeur (*p)->next conservé dans le nœud à supprimer est dissocié (par release, ce qui retourne le pointeur mais rend le point d'origine nul) avant reset (implicitement) détruit ce noeud (quand il détruit l'ancienne valeur détenue par p), en s'assurant qu'un et un seul noeud est détruit à cette époque. (Dans la variante mentionnée dans le commentaire, ce minutage serait laissé aux internes de la mise en oeuvre de l'opérateur d'affectation de mouvements de l'instance std::unique_ptr de list; la norme dit 20.7.1.2.3; 2 que cet opérateur agisse "comme si en appelant reset(u.release())", le minutage serait alors sûr ici également.)

Notez que prepend et remove_first ne peuvent pas être appelés par les clients stockant une variable locale node pour une liste toujours non vide, et à juste titre puisque les implémentations données ne peuvent pas fonctionner dans de tels cas.

Mode 3: passer un pointeur intelligent par référence rvalue (modifiable)

C'est le mode préféré à utiliser lors de la prise de possession du pointeur. Je souhaite appeler cette méthode par chèque : le demandeur doit accepter de renoncer à la propriété, comme s'il fournissait de l'argent, en signant le chèque, mais le retrait reporté jusqu'à ce que la fonction appelée vole réellement le pointeur (exactement comme si vous utilisiez le mode 2). La "signature du chèque" signifie concrètement que les appelants doivent envelopper un argument dans std::move (comme dans le mode 1) s’il s’agit d’une valeur (si c’est une valeur, la partie "abandon de propriété" est évidente et nécessite pas de code séparé).

Notez que techniquement, le mode 3 se comporte exactement comme le mode 2, de sorte que la fonction appelée ne doit pas nécessairement assumer la propriété. Cependant, je voudrais insister sur le fait que s'il existe une incertitude quant au transfert de propriété (en utilisation normale), le mode 2 doit être préféré au mode 3, de sorte que l'utilisation du mode 3 indique implicitement aux appelants qu'ils sont abandonner la propriété. On pourrait rétorquer que seul le passage en mode 1 des arguments signale réellement une perte de propriété forcée pour les appelants. Mais si un client a des doutes sur les intentions de la fonction appelée, il est supposé connaître les spécifications de la fonction appelée, ce qui devrait dissiper tout doute.

Il est étonnamment difficile de trouver un exemple typique impliquant notre type list qui utilise la transmission d’argument en mode 3. Déplacer une liste b à la fin d'une autre liste a est un exemple typique. cependant a (qui survit et conserve le résultat de l'opération) est mieux passé en mode 2:

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

Voici un exemple pur de passage d’arguments en mode 3: il prend une liste (et son propriétaire) et renvoie une liste contenant les nœuds identiques dans l’ordre inverse.

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

Cette fonction peut être appelée comme dans l = reversed(std::move(l)); pour inverser la liste, mais la liste inversée peut également être utilisée différemment.

Ici, l’argument est immédiatement déplacé vers une variable locale pour plus d’efficacité (on aurait pu utiliser le paramètre l directement à la place de p, mais y accéder à chaque fois impliquerait un niveau supplémentaire d’indirection); par conséquent, la différence avec le passage des arguments en mode 1 est minime. En fait, en utilisant ce mode, l’argument aurait pu servir directement de variable locale, évitant ainsi ce déplacement initial; Ceci est juste une instance du principe général suivant: si un argument passé par référence sert uniquement à initialiser une variable locale, vous pouvez également le transmettre par valeur et utiliser le paramètre comme variable locale.

L'utilisation du mode 3 semble être préconisée par la norme, comme en témoigne le fait que toutes les fonctions de bibliothèque fournies transférant la propriété des pointeurs intelligents utilisant le mode 3. Un exemple particulièrement convaincant est le constructeur std::shared_ptr<T>(auto_ptr<T>&& p). Ce constructeur utilisait (dans std::tr1) pour prendre une référence modifiable lvalue (tout comme le constructeur de copie auto_ptr<T>&), et pouvait donc être appelé avec un auto_ptr<T> lvalue p comme dans std::shared_ptr<T> q(p), après quoi p a été réinitialisé à null. En raison du passage du mode 2 au mode 3 en passant d'arguments, cet ancien code doit maintenant être réécrit pour std::shared_ptr<T> q(std::move(p)) et continuera ensuite à fonctionner. Je comprends que le comité n’a pas aimé le mode 2 ici, mais il a eu la possibilité de passer au mode 1, en définissant std::shared_ptr<T>(auto_ptr<T> p), il aurait pu s’assurer que le code ancien fonctionne sans modification, car (contrairement aux pointeurs uniques ) les auto-pointeurs peuvent être déréférencés silencieusement à une valeur (l'objet pointeur étant lui-même réinitialisé à une valeur nulle dans le processus). Apparemment, les membres du comité ont tellement préféré le mode 3 que le mode 1 qu'ils ont choisi de casser activement le code existant plutôt que d'utiliser le mode 1, même pour un usage déjà déconseillé.

Quand préférer le mode 3 au mode 1

Le mode 1 est parfaitement utilisable dans de nombreux cas et peut être préféré au mode 3 dans les cas où la prise en charge de la propriété prendrait autrement la forme de déplacer le pointeur intelligent vers une variable locale, comme dans l'exemple reversed ci-dessus. Cependant, je peux voir deux raisons de préférer le mode 3 dans le cas plus général:

  • Il est légèrement plus efficace de passer une référence que de créer un pointage temporaire et de supprimer l’ancien pointeur (le traitement de l’argent est quelque peu laborieux); dans certains cas, le pointeur peut être passé plusieurs fois inchangé à une autre fonction avant d'être volé. De tels passages nécessiteront généralement l'écriture std::move (sauf si le mode 2 est utilisé), mais notez qu'il s'agit simplement d'un casting qui ne fait rien (en particulier, aucun déréférencement), de sorte que son coût est nul.

  • Doit-on imaginer que quelque chose jette une exception entre le début de l'appel de fonction et le point où il (ou un appel contenu) déplace réellement l'objet pointé dans une autre structure de données (et cette exception n'est pas déjà prise dans la fonction elle-même ), puis en utilisant le mode 1, l'objet référencé par le pointeur intelligent sera détruit avant qu'une clause catch ne puisse gérer l'exception (car le paramètre de fonction a été détruit lors du déroulement de la pile), mais pas lors de l'utilisation du mode 3 Ce dernier donne à l'appelant la possibilité de récupérer les données de l'objet dans de tels cas (en interceptant l'exception). Notez que le mode 1 ici ne provoque pas de fuite de mémoire , mais peut entraîner une perte irrémédiable de données pour le programme, ce qui peut également être indésirable.

Renvoi d'un pointeur intelligent: toujours par valeur

Pour conclure un mot sur , renvoyant un pointeur intelligent, pointant probablement sur un objet créé pour être utilisé par l'appelant. Ce n'est pas vraiment un cas comparable à passer des pointeurs dans des fonctions, mais pour être complet, je voudrais insister sur le fait que dans de tels cas retournent toujours par valeur (et ne pas utiliser std::move dans l'instruction return. Personne ne veut obtenir une référence à un pointeur qui vient probablement d'être mis en attente.

52
Marc van Leeuwen

Oui, vous devez le faire si vous prenez le unique_ptr par valeur dans le constructeur. L'explicité est une bonne chose. Puisque unique_ptr est incopeable (copie privée), ce que vous avez écrit devrait vous donner une erreur de compilation.

4
Xeo

Edit: Cette réponse est fausse, même si, à proprement parler, le code fonctionne. Je le laisse seulement ici parce que la discussion est trop utile. Cette autre réponse est la meilleure réponse donnée au moment de ma dernière modification: Comment passer un argument unique_ptr à un constructeur ou à une fonction?

L'idée de base de ::std::move est que les personnes qui vous transmettent le unique_ptr l'utilisent pour exprimer le fait de savoir qu'elles savent que le unique_ptr perdra la propriété.

Cela signifie que vous devriez utiliser une référence rvalue à un unique_ptr dans vos méthodes, pas à un unique_ptr lui-même. Cela ne fonctionnera de toute façon pas parce que passer dans un vieux unique_ptr ordinaire nécessiterait de faire une copie, ce qui est explicitement interdit dans l'interface pour unique_ptr. Il est intéressant de noter que l’utilisation d’une référence rvalue nommée la reconvertit en une lvalue. Vous devez donc également utiliser ::std::move dans .

Cela signifie que vos deux méthodes devraient ressembler à ceci:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Ensuite, les personnes utilisant les méthodes feraient ceci:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Comme vous le voyez, le ::std::move indique que le pointeur va perdre la propriété au moment où il est le plus pertinent et utile de le savoir. Si cela se produisait de manière invisible, il serait très déroutant pour ceux qui utilisent votre classe de perdre objptr soudainement la propriété sans raison apparente.

3
Omnifarious

Au sommet voté réponse. Je préfère passer par référence rvalue.

Je comprends le problème que peut poser le passage par référence à une valeur. Mais divisons ce problème en deux parties:

  • pour l'appelant:

Je dois écrire le code Base newBase(std::move(<lvalue>)) ou Base newBase(<rvalue>).

  • pour l'appelé:

L'auteur de la bibliothèque doit garantir qu'il déplacera en réalité unique_ptr pour initialiser le membre s'il souhaite en posséder la propriété.

C'est tout.

Si vous passez par référence rvalue, il ne fera appel qu'à une instruction "move", mais si passe par valeur, il s'agit de deux.

Oui, si l'auteur de la bibliothèque n'est pas expert en la matière, il ne peut pas déplacer unique_ptr pour initialiser le membre, mais c'est le problème de l'auteur, pas de vous. Quoi qu’il en soit par valeur ou par référence à la valeur, votre code est le même!

Si vous écrivez une bibliothèque, vous savez maintenant que vous devriez la garantir, alors faites-le simplement, passer par référence à valeur est un meilleur choix que valeur. Le client qui utilise votre bibliothèque écrit simplement le même code.

Maintenant, pour votre question. Comment passer un argument unique_ptr à un constructeur ou à une fonction?

Vous savez quel est le meilleur choix.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

0
merito
Base(Base::UPtr n):next(std::move(n)) {}

devrait être beaucoup mieux comme

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

et

void setNext(Base::UPtr n)

devrait être

void setNext(Base::UPtr&& n)

avec le même corps.

Et ... qu'est-ce que evt dans handle() ??

0
Emilio Garavaglia