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

400

Je suis nouveau pour déplacer la sémantique en C ++ 11 et je ne sais pas très bien comment gérer les unique_ptrparamètres dans les constructeurs ou les fonctions. Considérez cette classe se référençant 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 ainsi que je dois écrire des fonctions prenant des unique_ptrarguments?

Et dois-je utiliser std::movedans le code appelant?

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

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
codablank1
la source
1
N'est-ce pas un défaut de segmentation car vous appelez b1-> setNext sur un pointeur vide?
balki

Réponses:

836

Voici les façons possibles de prendre un pointeur unique comme argument, ainsi que leur signification associée.

(A) Par valeur

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

Pour que l'utilisateur puisse appeler cela, 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 transférez la propriété du pointeur sur la fonction / l'objet / etc en question. Une fois newBaseconstruit, nextBaseest garanti vide . Vous n'êtes pas propriétaire de l'objet, et vous n'avez même plus de pointeur vers lui. C'est parti.

Ceci est assuré car nous prenons le paramètre par valeur. std::movene bouge en fait rien; 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.

Parce que Base::Base(std::unique_ptr<Base> n)prend son argument par valeur plutôt que par référence de valeur r, 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 déplace réellement la valeur de nextBasedans l'argument de 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 réelle l (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 celle de toute autre utilisation de références non const: la fonction peut ou non revendiquer la propriété du pointeur. Compte tenu de ce code:

Base newBase(nextBase);

Il n'y a aucune garantie nextBasevide. Il peut être vide; il se peut que non. Cela dépend vraiment de ce Base::Base(std::unique_ptr<Base> &n)que l' on veut faire. Pour cette raison, il n'est pas très évident, juste à partir de la signature de la fonction, de ce qui va se passer; 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 de valeur const 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 a const&, vous dites que la fonction peut accéder au Basevia le pointeur, mais qu'elle ne peut la stocker nulle part. Il ne peut en revendiquer la propriété.

Cela peut être utile. Pas nécessairement pour votre cas spécifique, mais il est toujours bon de pouvoir remettre un pointeur à quelqu'un et de savoir qu'il ne peut pas (sans enfreindre les règles du C ++, comme ne pas rejeter const) en revendiquer la propriété. Ils ne peuvent pas le stocker. Ils peuvent le transmettre à d'autres, mais ces autres 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 de valeur l non constante". 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::movelors du passage d'arguments non temporaires.

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

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

Vous avez une attente raisonnable qui, une fois cette ligne terminée, nextBasedoit être vide. Il aurait dû être déplacé. Après tout, vous avez çastd::move assis là, vous disant qu'un mouvement s'est produit.

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

Recommandations

  • (A) Par valeur: si vous voulez qu'une fonction revendique la propriété de a unique_ptr, prenez-la par valeur.
  • (C) Par référence const l-value: Si vous voulez qu'une fonction utilise simplement le unique_ptrpour la durée de l'exécution de cette fonction, prenez-la const&. Vous pouvez également passer un &ou const&au type réel 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é (selon les chemins de code internes), alors prenez-la &&. Mais je déconseille fortement de le faire dans la mesure du possible.

Comment manipuler unique_ptr

Vous ne pouvez pas copier un fichier unique_ptr. Vous ne pouvez que le déplacer. La bonne façon de le faire est d' std::moveutiliser la fonction de bibliothèque standard.

Si vous prenez une unique_ptrvaleur par, 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));

Il s'agit en réalité de deux déclarations:

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

(Remarque: le code ci-dessus ne se 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).

Le temporaryest juste une référence de valeur r à oldPtr. C'est dans le constructeur de l' newPtrendroit où le mouvement se produit. unique_ptrLe constructeur de mouvement (un constructeur qui prend un &&pour lui-même) est ce que fait le mouvement réel.

Si vous avez une unique_ptrvaleur et que vous souhaitez la stocker quelque part, vous devez utiliser std::movepour effectuer le stockage.

Nicol Bolas
la source
5
@Nicol: mais std::movene nomme pas sa valeur de retour. N'oubliez pas que les références rvalue nommées sont des lvalues. ideone.com/VlEM3
R. Martinho Fernandes
31
Je suis fondamentalement d'accord avec cette réponse, mais j'ai quelques remarques. (1) Je ne pense pas qu'il existe un cas d'utilisation valide pour passer une référence à const lvalue: tout ce que l'appelé pourrait faire avec cela, il peut aussi le faire en référence au pointeur const (nu), ou mieux encore le pointeur lui-même [et ce n'est pas son affaire de savoir que la propriété est détenue par le biais d'un unique_ptr; peut-être que certains autres appelants ont besoin de la même fonctionnalité mais tiennent un shared_ptrappel à la place] (2) par référence lvalue pourrait être utile si la fonction appelée modifie le pointeur, par exemple, en ajoutant ou en supprimant des nœuds (appartenant à la liste) d'une liste liée.
Marc van Leeuwen
8
... (3) Bien que votre argument favorisant le passage par la valeur plutôt que le passage par la référence rvalue soit logique, je pense que la norme elle-même transmet toujours les unique_ptrvaleurs par la référence rvalue (par exemple lors de leur transformation shared_ptr). La raison pourrait être qu'il est légèrement plus efficace (aucun déplacement vers des pointeurs temporaires n'est effectué) alors qu'il donne exactement les mêmes droits à l'appelant (peut transmettre des valeurs r ou des valeurs l enveloppées std::move, mais pas des valeurs nues).
Marc van Leeuwen
19
Juste pour répéter ce que Marc a dit, et en citant Sutter : "N'utilisez pas un const unique_ptr & comme paramètre; utilisez plutôt un widget *"
Jon
17
Nous avons découvert un problème avec la sous-valeur - le déplacement a lieu pendant l'initialisation des arguments, qui n'est pas ordonné par rapport aux autres évaluations d'arguments (sauf dans une liste initializer_list, bien sûr). Alors que l'acceptation d'une référence rvalue ordonne fortement que le déplacement se produise après l'appel de fonction, et donc après l'évaluation d'autres arguments. Il est donc préférable d'accepter la référence rvalue chaque fois que la propriété sera prise.
Ben Voigt
57

Permettez-moi d'essayer d'énoncer les différents modes viables de passage de pointeurs vers des objets dont la mémoire est gérée par une instance du std::unique_ptrmodèle de classe; il s'applique également à l'ancien std::auto_ptrmodèle de classe (qui, je crois, autorise toutes les utilisations de ce pointeur unique, mais pour lequel en outre des valeurs modifiables seront 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 être autorisées à partager des parties avec d'autres instances ou à être circulaires) appartiennent entièrement à la personne qui détient le listpointeur initial . Si le code client sait que la liste qu'il stocke ne sera jamais vide, il peut également choisir de stocker la première nodedirectement plutôt que a list. Aucun destructeur pour les nodebesoins à définir: puisque les destructeurs pour ses champs sont automatiquement appelés, la liste entière sera supprimée récursivement par le destructeur du pointeur intelligent une fois la durée de vie du pointeur ou du nœud initial terminée.

Ce type récursif donne l'occasion de discuter de certains cas qui sont moins visibles dans le cas d'un pointeur intelligent vers des données simples. De plus, les fonctions elles-mêmes fournissent occasionnellement (récursivement) un exemple de code client. Le typedef pour listest bien sûr biaisé unique_ptr, mais la définition pourrait être modifiée pour être utilisée auto_ptrou à la shared_ptrplace sans qu'il soit nécessaire de modifier ce qui est dit ci-dessous (notamment concernant la sécurité des exceptions étant assurée sans avoir besoin d'écrire des destructeurs).

Modes de passage des pointeurs intelligents

Mode 0: passez 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 à qui appartient 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 la forme la plus flexible, car indépendamment de la propriété, un client peut toujours produire un pointeur brut (soit en appelant la getméthode, soit à partir de l'opérateur d'adresse &).

Par exemple, la fonction pour calculer la longueur d'une telle liste, ne doit pas être un listargument, 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 contient une variable list headpeut appeler cette fonction comme length(head.get()), tandis qu'un client qui a choisi de stocker une node nliste non vide peut appeler length(&n).

Si le pointeur est garanti non nul (ce qui n'est pas le cas ici car les listes peuvent être vides), on pourrait 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- constsi la fonction doit mettre à jour le contenu du ou des nœuds, sans ajouter ou supprimer aucun d'entre eux (ce dernier impliquerait la propriété).

Un cas intéressant qui tombe dans la catégorie du mode 0 est de faire une copie (approfondie) de la liste; si une fonction qui le fait doit naturellement transférer la propriété de la copie qu'elle crée, elle ne se préoccupe pas de la propriété de la liste qu'elle copie. Il pourrait donc ê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 un examen attentif, à la fois pour la question de savoir pourquoi il compile (le résultat de l'appel récursif à copydans 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 nextchamp de la généré node), et pour la question de savoir pourquoi il est à l'abri des exceptions (si pendant le processus d'allocation récursive la mémoire s'épuise et un appel de newlancers std::bad_alloc, alors à ce moment-là, un pointeur vers la liste partiellement construite est conservé de manière anonyme dans un temporaire de type listcréé pour la liste d'initialisation, et son destructeur nettoiera cette liste partielle). Soit dit en passant, il faudrait résister à la tentation de remplacer (comme je l'ai fait initialement) le second nullptrparp, qui après tout est connu pour être nul à ce stade: on ne peut pas construire un pointeur intelligent à partir d'un pointeur (brut) vers constant , même s'il est connu pour être nul.

Mode 1: passer un pointeur intelligent par valeur

Une fonction qui prend une valeur de pointeur intelligent comme argument prend immédiatement possession de l'objet pointé: le pointeur intelligent que l'appelant détenait (que ce soit dans une variable nommée ou dans un temporaire anonyme) 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'un temporaire, la copie peut avoir été éludée, mais dans tous les cas, l'appelant a perdu l'accès à l'objet pointé). Je voudrais appeler cet appel de mode en espèces : l'appelant paie d'avance le service appelé et ne peut se faire aucune illusion sur la propriété après l'appel. Pour que cela soit clair, les règles de langage exigent que l'appelant encapsule l'argument dansstd::movesi le pointeur intelligent est maintenu dans une variable (techniquement, si l'argument est une valeur l); 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 valeur temporaire, en laissant la variable nulle.

Dans les cas où la fonction appelée s'approprie inconditionnellement (pilfers) l'objet pointé, ce mode utilisé avec std::unique_ptrou std::auto_ptrest un bon moyen de passer un pointeur avec sa propriété, ce qui évite tout risque de fuite de mémoire. Néanmoins, je pense qu'il n'y a que très peu de situations où le mode 3 ci-dessous ne doit pas être préféré (très légèrement) au mode 1. Pour cette raison, je ne fournirai aucun exemple d'utilisation de ce mode. (Mais voir l' reversedexemple du mode 3 ci-dessous, où l'on remarque que le mode 1 ferait au moins aussi bien.) Si la fonction prend plus d'arguments que ce pointeur, il peut arriver qu'il y ait en plus une raison technique pour éviter le mode 1 (avec std::unique_ptrou std::auto_ptr): puisqu'une opération de déplacement réelle a lieu lors du passage d'une variable de pointeurppar l'expression détient une valeur utile lors de l'évaluation des autres arguments (l'ordre d'évaluation étant non spécifié), ce qui pourrait conduire à des erreurs subtiles; en revanche, l'utilisation du mode 3 garantit qu'aucun déplacement de n'a lieu avant l'appel de fonction, de sorte que d'autres arguments peuvent accéder en toute sécurité à une valeur via .std::move(p) , on ne peut pas supposer queppp

Lorsqu'il est utilisé avec std::shared_ptr, ce mode est intéressant car avec une seule définition de fonction, il permet à l'appelant de choisir de conserver ou non une copie de partage du pointeur tout en créant une nouvelle copie de partage à utiliser par la fonction (cela se produit lorsqu'une valeur est fourni; le constructeur de copie pour les pointeurs partagés utilisés lors de l'appel augmente le nombre de références), ou pour simplement donner à la fonction une copie du pointeur sans en conserver un ou sans toucher le nombre de références (cela se produit lorsqu'un argument rvalue est fourni, éventuellement une valeur l enveloppé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 de fonction différant uniquement en ce que la première version invoque la sémantique de copie (en utilisant la construction / affectation de copie lors de l'utilisation x) mais la deuxième version déplace la sémantique (en écrivant à la std::move(x)place, comme dans l'exemple de code). Ainsi, pour les pointeurs partagés, le mode 1 peut être utile pour éviter une certaine duplication de code.

Mode 2: passer un pointeur intelligent par référence de valeur (modifiable)

Ici, la fonction nécessite simplement d'avoir une référence modifiable au pointeur intelligent, mais ne donne aucune indication de ce qu'elle en fera. Je voudrais appeler cette méthode appel par carte : l'appelant assure le paiement en donnant un numéro de carte bancaire. 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 lvalue modifiable, correspondant au fait que l'effet souhaité de la fonction peut inclure de laisser une valeur utile dans la variable 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 un constantelvalue référence (se référant à un temporaire) à partir d'une rvalue. (Contrairement à la situation opposée gérée par std::move, un cast de Y&&à Y&, avec Yle type de pointeur intelligent, n'est pas possible; néanmoins, cette conversion peut être obtenue par une simple fonction de modèle si vous le souhaitez vraiment; voir https://stackoverflow.com/a/24868376 / 1436796 ). Dans le cas où la fonction appelée entend s'approprier inconditionnellement l'objet en dérobant 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 à l'intérieur de notre fonction mais demande aux appelants de fournir une valeur r, devrait être préféré pour une telle utilisation.

Cependant, il existe un cas d'utilisation valide pour le mode 2, à savoir les fonctions qui peuvent modifier le pointeur ou l'objet pointé d' une manière qui implique la propriété . Par exemple, une fonction qui préfixe un nœud à un listfournit un exemple d'une telle utilisation:

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

De toute évidence, 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 que différente de celle d'avant.

Encore une fois, il est intéressant d'observer ce qui se passe si l' prependappel échoue par manque de mémoire libre. Ensuite, l' newappel sera lancé std::bad_alloc; à ce stade, comme aucun noden'a pu être alloué, il est certain que la référence de valeur r passée (mode 3) de std::move(l)ne peut pas encore être volée, car cela serait fait pour construire le nextchamp de celui nodequi n'a pas été alloué. Ainsi, le pointeur intelligent d'origine lcontient toujours la liste d'origine lorsque l'erreur est levée; cette liste sera soit correctement détruite par le destructeur de pointeur intelligent, ou au cas où ldevrait survivre grâce à une catchclause suffisamment précoce , elle contiendra toujours la liste d'origine.

C'était un exemple constructif; avec un clin d'oeil à cette question, on peut également donner l'exemple le plus destructeur de suppression du 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, l'exactitude est assez subtile ici. Notamment, dans la déclaration finale, le pointeur (*p)->nextdétenu à l'intérieur du nœud à supprimer n'est pas lié (par release, qui renvoie le pointeur mais rend la valeur nulle d'origine) avant reset (implicitement) de détruire ce nœud (lorsqu'il détruit l'ancienne valeur détenue par p), garantissant que un et un seul nœud est détruit à ce moment. (Dans la forme alternative mentionnée dans le commentaire, ce délai serait laissé aux internes de la mise en œuvre de l'opérateur d'affectation de déplacement de l' std::unique_ptrinstance list; la norme dit 20.7.1.2.3; 2 que cet opérateur devrait agir "comme si par appeler reset(u.release())", d'où le moment devrait être sûr ici aussi.)

Notez que prepend et remove_firstne peuvent pas être appelés par les clients qui stockent une nodevariable locale pour une liste toujours non vide, et à juste titre car les implémentations données ne peuvent pas fonctionner dans de tels cas.

Mode 3: passer un pointeur intelligent par une référence de valeur (modifiable)

Il s'agit du mode préféré à utiliser lorsque vous prenez simplement possession du pointeur. Je voudrais appeler cette méthode appel par chèque : l'appelant doit accepter de renoncer à la propriété, comme s'il fournissait de l'argent, en signant le chèque, mais le retrait réel est reporté jusqu'à ce que la fonction appelée vole le pointeur (exactement comme lors de l'utilisation du mode 2). ). La "signature du chèque" signifie concrètement que les appelants doivent encapsuler un argument std::move(comme dans le mode 1) s'il s'agit d'une valeur l (s'il s'agit d'une valeur r, la partie "abandonner la propriété" est évidente et ne nécessite pas de code séparé).

Notez que techniquement le mode 3 se comporte exactement comme le mode 2, donc la fonction appelée n'a pas à assumer la propriété; Cependant, j'insiste sur le fait qu'en cas d'incertitude concernant le transfert de propriété (en utilisation normale), le mode 2 devrait être préféré au mode 3, de sorte que l'utilisation du mode 3 soit implicitement un signal aux appelants qu'ils sont renoncent à la propriété. On pourrait rétorquer que seul l'argument du mode 1 passant signale vraiment une perte forcée de propriété aux appelants. Mais si un client a des doutes sur les intentions de la fonction appelée, il est censé connaître les spécifications de la fonction appelée, ce qui devrait lever tout doute.

Il est étonnamment difficile de trouver un exemple typique impliquant notre listtype qui utilise le passage d'arguments en mode 3. Déplacer une liste bà la fin d'une autre liste aest un exemple typique; cependant a(qui survit et détient le résultat de l'opération) est mieux passé en utilisant le 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
}

Un exemple pur de passage d'arguments en mode 3 est le suivant qui prend une liste (et sa propriété), et retourne 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 l = reversed(std::move(l));pour inverser la liste en elle-même, 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 l'efficacité (on aurait pu utiliser le paramètre ldirectement à 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 du paramètre mode 1 est minime. En fait, en utilisant ce mode, l'argument aurait pu servir directement de variable locale, évitant ainsi ce mouvement initial; ceci est juste une instance du principe général selon lequel si un argument passé par référence ne sert qu'à initialiser une variable locale, on pourrait tout aussi bien le passer 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 qui transfèrent la propriété des pointeurs intelligents en utilisant le mode 3. Un exemple particulièrement convaincant est le constructeur std::shared_ptr<T>(auto_ptr<T>&& p). Ce constructeur utilisait (in std::tr1) pour prendre une référence lvalue modifiable (tout comme le auto_ptr<T>&constructeur de copie), et pouvait donc être appelé avec une auto_ptr<T>lvalue pcomme in std::shared_ptr<T> q(p), après quoi il pa été réinitialisé à null. En raison du passage du mode 2 au mode 3 lors du passage d'arguments, cet ancien code doit maintenant être réécrit enstd::shared_ptr<T> q(std::move(p)) et continuera à fonctionner. Je comprends que le comité n'aimait pas le mode 2 ici, mais il avait la possibilité de passer au mode 1, en définissantstd::shared_ptr<T>(auto_ptr<T> p)au lieu de cela, ils auraient pu garantir que l'ancien code fonctionne sans modification, car (contrairement aux pointeurs uniques) les pointeurs automatiques peuvent être silencieusement déréférencés à une valeur (l'objet pointeur lui-même étant réinitialisé à null dans le processus). Apparemment, le comité a tellement préféré défendre le mode 3 plutôt que le mode 1, qu'il a choisi de casser activement le code existant plutôt que d'utiliser le mode 1 même pour un usage déjà obsolète.

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

Le mode 1 est parfaitement utilisable dans de nombreux cas, et pourrait être préféré au mode 3 dans les cas où supposer que la propriété prendrait autrement la forme de déplacer le pointeur intelligent vers une variable locale comme dans l' reversedexemple 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 transmettre une référence que de créer un temporaire et de supprimer l'ancien pointeur (la gestion de l'argent est quelque peu laborieuse); dans certains scénarios, le pointeur peut être transmis plusieurs fois sans changement à une autre fonction avant d'être réellement volé. Un tel passage nécessitera généralement l'écriture std::move(sauf si le mode 2 est utilisé), mais notez qu'il ne s'agit que d'un cast qui ne fait rien (en particulier pas de déréférencement), donc il n'a aucun coût attaché.

  • Devrait-il être concevable que quelque chose lève 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é vers une autre structure de données (et cette exception n'est pas déjà interceptée à l'intérieur de la fonction elle-même ), puis lors de l'utilisation du mode 1, l'objet référencé par le pointeur intelligent sera détruit avant qu'une catchclause 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 la l'appelant a 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écupérable de données pour le programme, ce qui peut également être indésirable.

Renvoyer un pointeur intelligent: toujours par valeur

Pour conclure un mot sur le retour d' un pointeur intelligent, pointant vraisemblablement vers 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, toujours retourner par valeur (et ne pas utiliser std::move dans l' returninstruction). Personne ne veut obtenir une référence à un pointeur qui vient probablement d'être supprimé.

Marc van Leeuwen
la source
1
+1 pour le Mode 0 - en passant le pointeur sous-jacent au lieu de unique_ptr. Légèrement hors sujet (puisque la question concerne le passage d'un unique_ptr) mais c'est simple et évite les problèmes.
Machta
"le mode 1 ici ne provoque pas de fuite de mémoire " - cela implique que le mode 3 provoque une fuite de mémoire, ce qui n'est pas vrai. Indépendamment du fait qu'il unique_ptrait été déplacé ou non, il supprimera toujours bien la valeur s'il la conserve toujours chaque fois qu'il est détruit ou réutilisé.
rustyx
@ RustyX: Je ne vois pas comment vous interprétez cette implication, et je n'ai jamais eu l'intention de dire ce que vous pensez que j'impliquais. Tout ce que je voulais dire, c'est que comme ailleurs, l'utilisation de unique_ptrprévient une fuite de mémoire (et remplit donc en quelque sorte son contrat), mais ici (c'est-à-dire en utilisant le mode 1), cela pourrait provoquer (dans des circonstances spécifiques) quelque chose qui pourrait être considéré comme encore plus nocif. , à savoir une perte de données (destruction de la valeur pointée) qui aurait pu être évitée en utilisant le mode 3.
Marc van Leeuwen
4

Oui, vous devez le faire si vous prenez la unique_ptrvaleur par dans le constructeur. L'explicité est une bonne chose. Comme il unique_ptrest non copiable (ctor de copie privée), ce que vous avez écrit devrait vous donner une erreur de compilation.

Xeo
la source
3

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

L'idée de base ::std::moveest que les gens qui vous dépassent unique_ptrdevraient l'utiliser pour exprimer la connaissance qu'ils savent unique_ptrqu'ils vont perdre la propriété.

Cela signifie que vous devez utiliser une référence rvalue à un unique_ptrdans vos méthodes, pas un unique_ptrlui - même. Cela ne fonctionnera pas de toute façon, car le passage dans un ancien simple unique_ptrnécessiterait de faire une copie, ce qui est explicitement interdit dans l'interface de unique_ptr. Il est intéressant de noter que l'utilisation d'une référence rvalue nommée la transforme à nouveau en lvalue, vous devez donc également l'utiliser ::std::move dans vos méthodes.

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 le feraient:

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::moveexprime que le pointeur va perdre sa propriété au point 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 les personnes utilisant votre classe d'avoir objptrsoudainement perdu la propriété sans raison apparente.

Omnifarious
la source
2
Les références rvalue nommées sont des lvalues.
R. Martinho Fernandes
êtes-vous sûr que oui Base fred(::std::move(objptr));et non Base::UPtr fred(::std::move(objptr));?
codablank1
1
Pour ajouter à mon commentaire précédent: ce code ne se compilera pas. Vous devez toujours utiliser std::movedans l'implémentation du constructeur et de la méthode. Et même lorsque vous passez par valeur, l'appelant doit toujours utiliser std::movepour passer lvalues. La principale différence étant qu'avec une valeur de passage, cette interface indique clairement que la propriété sera perdue. Voir le commentaire de Nicol Bolas sur une autre réponse.
R. Martinho Fernandes
@ codablank1: Oui. Je montre comment utiliser le constructeur et les méthodes de base qui prennent des références rvalue.
Omnifarious
@ R.MartinhoFernandes: Oh, intéressant. Je suppose que cela a du sens. Je m'attendais à ce que vous vous trompiez, mais les tests réels ont prouvé que vous aviez raison. Fixé maintenant.
Omnifarious
0
Base(Base::UPtr n):next(std::move(n)) {}

devrait être beaucoup mieux que

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 ... ce qui est evten handle()??

Emilio Garavaglia
la source
3
Il n'y a aucun gain à utiliser std::forwardici: Base::UPtr&&est toujours un type de référence rvalue, et le std::movepasse comme rvalue. Il est déjà transmis correctement.
R. Martinho Fernandes
7
Je suis fortement en désaccord. Si une fonction prend une unique_ptrvaleur par, alors vous êtes assuré qu'un constructeur de déplacement a été appelé sur la nouvelle valeur (ou simplement que vous avez reçu une valeur temporaire). Cela garantit que la unique_ptrvariable dont dispose l'utilisateur est désormais vide . Si vous le prenez à la &&place, il ne sera vidé que si votre code appelle une opération de déplacement. À votre façon, il est possible que la variable dont l'utilisateur n'a pas été déplacé. Ce qui rend l'utilisation de l'utilisateur std::movesuspecte et déroutante. L'utilisation std::movedoit toujours garantir que quelque chose a été déplacé .
Nicol Bolas
@NicolBolas: Vous avez raison. Je vais supprimer ma réponse car pendant que cela fonctionne, votre observation est absolument correcte.
Omnifarious
0

À la première réponse votée. Je préfère passer par référence rvalue.

Je comprends quel est le problème du passage par la référence rvalue. Mais divisons ce problème de deux côtés:

  • pour l'appelant:

Je dois écrire du 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 réellement l'unique_ptr pour initialiser le membre s'il veut en devenir propriétaire.

C'est tout.

Si vous passez par référence rvalue, il n'appellera qu'une seule instruction "move", mais si vous passez par valeur, c'est deux.

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

Si vous écrivez une bibliothèque, vous savez maintenant que vous devez la garantir, alors faites-le, passer par la référence rvalue est un meilleur choix que la valeur. Le client qui utilise votre bibliothèque écrira 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

merito
la source