Comment passer un argument ptr unique à un constructeur ou à une fonction?

je suis nouveau pour déplacer la sémantique en 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érez 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 ainsi que je devrais écrire des fonctions prenant des arguments unique_ptr ?

et dois-je utiliser std::move dans le code d'appel?

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

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
331
demandé sur R. Martinho Fernandes 2011-11-13 23:58:49

6 réponses

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

(A) En Valeur

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

pour que l'utilisateur appelle ceci, il doit faire l'un des suivants:

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

prendre un pointeur unique par valeur signifie que vous êtes transférer propriété du pointeur à la fonction/objet/etc en question. Après newBase est construit, nextBase est garanti pour être vide . Vous ne possédez pas l'objet, et vous n'avez même plus de pointeur. Il s'en est allé.

ceci est garanti parce que nous prenons le paramètre en valeur. std::move ne fait pas déplacer quoi que ce soit, c'est juste une fantaisie de fonte. std::move(nextBase) renvoie un Base&& qui est une référence à la valeur r de nextBase . C'est tout ce qu'il fait.

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 réellement déplace la valeur de nextBase dans l'argument de fonction n .

(B) par référence à la valeur 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). On ne peut pas l'appeler avec un temporaire comme celui-ci:

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

le sens de ceci est le même que le sens de toute autre utilisation des références non-const: la fonction peut ou ne peut pas revendiquer la propriété du pointeur. Compte tenu de ce code:

Base newBase(nextBase);

il n'y a aucune garantie que nextBase soit vide. It mai doit être vide; il ne doit pas l'être. Cela dépend vraiment de ce que Base::Base(std::unique_ptr<Base> &n) veut faire. Pour cette raison, ce n'est pas très évident juste à partir de la signature de la fonction ce qui va se passer; vous devez lire la mise en œuvre (ou la documentation associée).

à cause de ça, Je ne suggérerais pas ça comme interface.

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

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

Je ne montre pas une implémentation, parce que vous ne peut pas passer d'un const& . En passant un const& , vous dites que la fonction peut accéder au Base via le pointeur, mais il ne peut pas stocker il n'importe où. Elle ne peut en revendiquer la propriété.

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

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

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

c'est plus ou moins identique au cas" par référence à une valeur non constante". Les différences sont deux choses.

  1. Vous peut pass temporaire:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
    
  2. Vous doit utiliser std::move lors du passage de la non-temporaire arguments.

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

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

vous pouvez raisonnablement vous attendre à ce que, une fois cette ligne terminée, nextBase soit vide. Elle aurait dû être déplacée. Après tout, vous avez ce std::move assis là, vous disant que le mouvement s'est produit.

Le problème est qu'il n'a pas. Il n'est pas garanti ont été déplacés. peut ont été déplacés, mais vous le saurez en regardant le code source. Vous ne pouvez pas le dire seulement à partir de la signature de la fonction.

recommandations

  • (A) Par valeur: si vous voulez dire pour une fonction à réclamation "1519570920 la" propriété , d'un unique_ptr , de le prendre par la valeur.
  • (C) par valeur de calcul référence: si vous voulez dire pour une fonction d'utiliser simplement le unique_ptr pour la durée d'exécution de cette fonction, prenez-le par const& . Alternativement, passer un & ou const& au type réel pointé, plutôt que d'utiliser un unique_ptr .
  • (D) Par valeur de r référence: si une fonction peut ou ne peut pas revendiquer la propriété (selon les chemins de code internes), alors prendre par && . Mais je vous déconseille fortement de le faire chaque fois que c'est possible.

comment manipuler unique_ptr

vous ne pouvez pas copier un unique_ptr . On ne peut que la déplacer. La bonne façon de le faire est d'utiliser la fonction de bibliothèque standard std::move .

Si vous prenez un unique_ptr par valeur, vous pouvez vous 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 affirmations:

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

(note: le code ci-dessus ne se compile pas techniquement, car les références non-temporaires de valeur de r ne sont pas en fait des valeurs de R. Il est ici pour les buts de démonstration seulement).

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

si vous avez une valeur unique_ptr et vous voulez la stocker quelque part, vous doit utiliser std::move pour faire le stockage.

705
répondu Nicol Bolas 2015-05-03 12:01:19

permettez-moi d'essayer d'énoncer les différents modes viables de passer des pointeurs autour d'objets dont la mémoire est gérée par une instance du modèle de classe std::unique_ptr ; il s'applique également à l'ancien modèle de classe std::auto_ptr (qui, je crois permet tous les usages que le pointeur unique fait, mais pour lequel en plus modifiables lvalues seront acceptées là où les valeurs R sont attendues, sans avoir à invoquer std::move ), et dans une certaine mesure aussi à 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) sont entièrement la propriété de 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 aussi choisir de stocker le premier node directement plutôt qu'un list . Pas de destructeur pour node doit être défini: puisque les destructeurs pour ses champs sont automatiquement appelés, la liste entière sera supprimée récursivement par le destructeur de pointeur intelligent une fois la durée de vie du pointeur initial ou du noeud terminé.

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. La définition de type pour list est biaisé bien sûr vers unique_ptr , mais la définition pourrait être changée à utiliser auto_ptr ou shared_ptr à la place sans beaucoup besoin de changer à ce qui est dit ci-dessous (notamment concernant la sécurité d'exception étant assurée sans la nécessité d'écrire des destructeurs).

Modes de passage de pointeurs intelligents autour de

Mode 0: passer un argument pointeur ou de référence à la place d'un pointeur intelligent

si votre fonction n'est pas concerné par la propriété, c'est la méthode préférée: n'en faites pas prendre un pointeur intelligent. Dans ce cas, votre fonction n'a pas besoin de s'inquiéter qui possède l'objet pointé vers, ou par quel moyen que la propriété est gérée, de sorte que 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 méthode get ou de l'adresse-de l'opérateur & ).

par exemple la fonction pour calculer la longueur d'une telle liste, ne devrait pas être donner 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 comme length(head.get()) , alors qu'un client qui a choisi de stocker un node n représentant une liste non vide peut appeler length(&n) .

Si le pointeur est garanti pour être non null (ce qui n'est pas le cas ici puisque la liste peut être vide) on pourrait préférer passer une référence plutôt qu'un pointeur. Il peut s'agir d'un pointeur/référence à non- const si la fonction a besoin de mettre à jour le contenu du(des) noeud (s), sans ajouter ou supprimer aucun d'entre eux (ce dernier impliquant la propriété).

un cas intéressant qui tombe dans la catégorie mode 0 est de faire une copie (profonde) de la liste; tandis qu'une fonction faisant ceci doit bien sûr transférer la propriété de la copie qu'il crée, il n'est pas concerné par le propriétaire de la liste il est la 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, tant pour la question de savoir pourquoi il compile du tout (le résultat de l'appel récursif à copy dans la liste des initialiseurs se lie à l'argument de référence rvalue dans le constructeur de mouvement de unique_ptr<node> , A. K. A. list , en initialisant le champ next du généré node ), et pour la question de savoir pourquoi il est exceptionnellement sûr (si pendant le processus d'allocation récursive la mémoire s'épuise et qu'un appel de new lance std::bad_alloc , alors un pointeur vers la liste partiellement construite est tenu anonymement dans un temporaire de type list créé pour la liste initialisante, et son destructeur nettoiera cette liste partielle). En passant, il faut résister à la tentation de remplacer (comme je l'ai fait initialement) le deuxième nullptr par p , qui après tout est connu pour être nul à ce point: on ne peut pas construire un pointeur intelligent à partir d'un pointeur (brut) à constante , même quand il est connu pour être nul.

Mode 1: passer un pointeur intelligent en valeur

une fonction qui prend une valeur de pointeur intelligent comme argument prend possession de l'objet pointé tout de suite: le pointeur intelligent que l'appelant a tenu (que ce soit dans une variable nommée ou un anonymous temporary) est copié dans la valeur de l'argument à l'entrée de la fonction et le pointeur de l'appelant est devenu nul (dans le cas d'un temporaire la copie pourrait 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 pour le service, et ne peut avoir aucune illusion sur la propriété après l'appel. Pour que cela soit clair, les règles de langage exigent que l'appelant argument dans std::move si le pointeur intelligent est tenu 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 à un temporaire, laissant la variable null.

pour les cas où la fonction appelée prend inconditionnellement la propriété de (pilfers) l'objet pointé, ce mode utilisé avec std::unique_ptr ou std::auto_ptr est un bon moyen de passer une pointeur avec sa propriété, ce qui évite tout risque de fuites 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é (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 noté 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 est en outre un raison technique pour éviter le mode 1 (avec std::unique_ptr ou std::auto_ptr ): puisqu'une opération de déplacement réelle a lieu tout en passant une variable indicatrice p par l'expression std::move(p) , on ne peut pas supposer que p détient une valeur utile tout en évaluant les autres arguments (l'ordre d'évaluation étant non précisé), ce qui pourrait conduire à des erreurs subtiles; en revanche, en utilisant le mode 3 assure qu'aucun déplacement de p a lieu avant la appel de fonction, pour que d'autres arguments puissent accéder en toute sécurité à une valeur par p .

Lorsqu'il est utilisé avec std::shared_ptr , ce mode est intéressant en ce qu'avec une définition de fonction unique il permet à l'appelant de choisir s'il doit conserver une copie de partage du pointeur pour lui-même tout en créant une nouvelle copie de partage pour être utilisé par la fonction (cela se produit quand un argument de valeur l est fourni; le constructeur de copie pour les pointeurs partagés utilisés à la call augmente le nombre de référence), ou de simplement donner à la fonction une copie du pointeur sans en conserver une ou toucher le nombre de référence (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
}

on pourrait obtenir la même chose en définissant séparément void f(const std::shared_ptr<X>& x) (pour le cas de lvalue) et void f(std::shared_ptr<X>&& x) (pour le cas de rvalue), avec des corps de fonction ne différant que dans la première version invoque la sémantique de la copie (en utilisant la construction de la copie/l'assignation en utilisant x ) mais la sémantique de déplacement de la deuxième version (en écrivant std::move(x) à la place, comme dans le code d'exemple). Ainsi, pour les pointeurs partagés, le mode 1 peut être utile pour éviter une certaine duplication du code.

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

ici, la fonction nécessite juste d'avoir une référence modifiable au pointeur intelligent, mais ne donne aucune indication de ce que il va faire avec elle. Je voudrais appeler cette méthode appel par carte : l'appelant assure le paiement en donnant un numéro de carte de crédit. La référence peut être utilisée pour prendre la propriété de l'objet pointé, mais il ne doit pas. Ce mode nécessite de fournir un argument modifiable de lvalue, correspondant au fait que l'effet désiré de la fonction peut inclure laisser une valeur utile dans la variable d'argument. Un interlocuteur avec une expression de valeur qu'il souhaite passer à une telle fonction serait forcé de le stocker dans une variable nommée pour pouvoir faire l'appel, puisque le langage ne fournit qu'une conversion implicite en une constante référence de valeur l (se référant à une temporaire) d'une valeur R. (Contrairement à la situation opposée traitée par std::move , une fonte de Y&& à Y& , avec Y le type pointeur intelligent, n'est pas possible; néanmoins, cette conversion pourrait être obtenue par une simple fonction de modèle si vraiment désiré; voir https://stackoverflow.com/a/24868376/1436796 ). Pour le cas où la fonction appelée entend prendre inconditionnellement la propriété de l'objet, en volant l'argument, l'obligation de fournir un argument de lvalue donne le mauvais signal: la variable n'aura aucune valeur utile après l'appel. Par conséquent, le mode 3, qui offre des possibilités identiques à l'intérieur de notre fonction, mais demande aux appelants de fournir une valeur, devrait être préféré pour une telle utilisation.

toutefois, il existe un cas d'utilisation valable pour le mode 2, à savoir des fonctions qui peuvent modifier le pointeur, ou l'objet pointé vers d'une manière qui implique la propriété . Par exemple, une fonction qui préfixes d'un nœud à un list fournit un exemple d'utilisation:

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

il est clair qu'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 celle d'avant.

encore une fois, il est intéressant d'observer ce qui se passe si l'appel prepend échoue faute de mémoire libre. Ensuite, l'appel new lancera std::bad_alloc ; à ce moment-là, puisqu'aucun node n'a pu être attribué, il est certain que la référence de valeur passée (mode 3) de std::move(l) ne peut pas encore avoir été pilfered, car cela être fait pour construire le champ next du node qui n'a pas été attribué. Ainsi, le pointeur intelligent original l conserve toujours la liste originale lorsque l'erreur est lancée; cette liste sera soit correctement détruite par le destructeur de pointeur intelligent, ou dans le cas où l devrait survivre grâce à une clause catch suffisamment précoce, il conservera la liste originale.

C'était un exemple constructif; avec un clin d'oeil à cette question on peut aussi donner l'exemple le plus destructeur de la suppression du premier noeud contenant une valeur donnée, s'il y en a:

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 justesse est assez subtile ici. Notamment, dans la déclaration finale, le pointeur (*p)->next placé à l'intérieur du noeud à supprimer est déverrouillé (par release , qui renvoie le pointeur mais rend l'original nul) avant reset (détruit implicitement) ce noeud (lorsqu'il détruit l'ancienne valeur détenue par p ), assurant qu'un et un seul noeud est détruit à ce moment. (Sous la forme alternative mentionnée dans le commentaire, ce timing serait laissé aux internes de la mise en oeuvre de l'opérateur d'assignation de mouvement de l'instance std::unique_ptr list ; la norme dit 20.7.1.2.3;2 que cet opérateur devrait agir "comme si en appelant reset(u.release()) ", d'où le timing devrait être sûr ici aussi.)

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

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

C'est le mode préféré à utiliser quand simplement prendre la propriété du pointeur. Je voudrais appeler cette méthode call by check : 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 pilfère réellement le pointeur (exactement comme il le ferait en utilisant le mode 2). La" signature du chèque "signifie concrètement que les appelants doivent envelopper un argument dans std::move (comme en mode 1) s'il s'agit d'une valeur l (s'il s'agit d'une valeur R, la partie" abandon de 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, de sorte que la fonction appelée n'a pas à assumer la propriété; cependant j'insisterais pour que s'il y a une incertitude sur le transfert de propriété (dans l'usage normal), le mode 2 devrait être préféré au mode 3, de sorte que l'utilisation du mode 3 est implicitement un signal pour les appelants qu'ils sont abandon de la propriété. On pourrait rétorquer que seul l'argument de mode 1 passant vraiment des signaux perte de propriété forcée pour les appelants. Mais si un client a des doutes sur les intentions de la fonction appelée, elle est censé connaître les spécifications de la fonction appelée, ce qui devrait lever le doute.

il est étonnamment difficile de trouver un exemple typique impliquant notre type list qui utilise le passage d'argument mode 3. Le déplacement d'une liste b à la fin d'une autre liste a est un exemple typique; cependant a (qui il est préférable de passer 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
}

un exemple pur de mode 3 de passage d'argument est le suivant qui prend une liste (et sa propriété), et renvoie une liste contenant les noeuds 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 en elle-même, mais la liste inversée peut également être utilisé différemment.

ici, l'argument est immédiatement déplacé vers une variable locale pour l'efficacité (on aurait pu utiliser le paramètre l directement à la place de p , mais alors y accéder à chaque fois impliquerait un niveau supplémentaire d'indirecte); par conséquent, la différence avec le mode 1 argument passant est minime. En fait, en utilisant ce mode, l'argument aurait pu servir directement de variable locale, évitant ainsi ce mouvement initial; c'est juste un exemple du principe général que si un l'argument passé par référence ne sert qu'à initialiser une variable locale, on pourrait tout aussi bien la passer par valeur à la place et utiliser le paramètre comme variable locale.

utilisant le mode 3 semble être prôné 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 utilisant le mode 3. Un exemple particulièrement convaincant est le constructeur std::shared_ptr<T>(auto_ptr<T>&& p) . Ce constructeur a utilisé (dans std::tr1 ) pour prendre un modifiable lvalue de référence (comme le auto_ptr<T>& constructeur de copie), et pourrait donc être appelée avec un auto_ptr<T> lvalue p , comme dans std::shared_ptr<T> q(p) , après quoi p a été réinitialisé à la valeur null. En raison du passage du mode 2 au mode 3 dans le passage de l'argument, cet ancien code doit maintenant être réécrit en std::shared_ptr<T> q(std::move(p)) et continuera alors à fonctionner. Je crois comprendre que le Comité n'a pas aimé le mode 2, mais il a eu la possibilité de passer à mode 1, en définissant std::shared_ptr<T>(auto_ptr<T> p) à la place, ils auraient pu s'assurer que l'ancien code fonctionne sans modification, parce que (contrairement à unique-pointers) les auto-pointeurs peuvent être discrètement 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é préconiser le mode 3 plutôt que le mode 1, qu'il a choisi de briser activement le code existant plutôt que d'utiliser le mode 1 même pour un usage déjà déprécié.

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ù assumer 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 pour créer un temporaire et nix le vieux pointeur (manipulation de l'argent est un peu laborieux); dans certains scénarios, le pointeur peut être passé plusieurs fois inchangé à une autre fonction avant qu'il ne soit réellement pilfered. Un tel passage nécessitera généralement l'écriture std::move (à moins que le mode 2 ne soit utilisé), mais notez que c'est juste une distribution qui ne fait rien (en particulier pas de déréférencement), de sorte qu'il a zéro coût attaché.

  • devrait-il être imaginable 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à pris à l'intérieur de la fonction elle-même), puis en utilisant le mode 1, l'objet auquel se réfère le pointeur intelligent sera détruit avant qu'une clause catch puisse gérer l'exception (parce que le paramètre de fonction a été détruit pendant le déroulement de la pile), mais pas si 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 capturant l'exception). Notez que le mode 1 ici ne cause pas de fuite de mémoire , mais peut conduire à une perte de données irrécouvrable pour le programme, qui pourrait être indésirable aussi bien.

Retourner un pointeur intelligent: toujours par valeur

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

44
répondu Marc van Leeuwen 2017-05-23 10:31:37

Oui, vous avez à le faire, si vous prenez le unique_ptr par la valeur dans le constructeur. Explicitement c'est une belle chose. Puisque unique_ptr est impossible à copier (copie privée ctor), ce que vous avez écrit devrait vous donner une erreur de compilateur.

4
répondu Xeo 2011-11-13 20:06:54

Edit: Cette réponse est fausse, même si, à proprement parler, le code fonctionne. Je la laisse ici parce que la discussion sous elle est trop utile. Cette autre réponse est la meilleure réponse donnée à la dernière fois que j'ai édité ceci: Comment passer un argument unique_ptr à un constructeur ou à une fonction?

l'idée de base de ::std::move est que les gens qui vous passent le unique_ptr devraient l'utiliser pour s'ils disent qu'ils savent que le unique_ptr dans lequel ils passent va perdre sa propriété.

cela signifie que vous devez utiliser une référence de valeur de R à un unique_ptr dans vos méthodes, pas un unique_ptr lui-même. Cela ne marchera pas de toute façon car passer dans un vieux unique_ptr nécessiterait de faire une copie, et c'est explicitement interdit dans l'interface pour unique_ptr . Ce qui est intéressant, c'est qu'en utilisant une référence rvalue nommée, on la transforme à nouveau en valeur L, donc vous devez utiliser ::std::move à l'intérieur 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); }

les gens utilisant les méthodes:

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 exprime que le pointeur va perdre la propriété au point où il est le plus pertinent et utile de savoir. Si cela se produisait invisiblement, ce serait très déroutant. pour les gens qui utilisent votre classe pour avoir objptr soudainement perdre la propriété pour aucune raison évidente.

2
répondu Omnifarious 2017-05-23 12:34:45
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... qu'est-ce que evt dans handle() ??

0
répondu Emilio Garavaglia 2011-11-13 20:08:30

Vers le haut voté réponse. Je préfère passer par référence rvalue.

je comprends Quel est le problème de passer par référence rvalue peut causer. 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 le destinataire de l'appel:

L'auteur de la bibliothèque doit garantir qu'il en fait, déplacez le unique_ptr pour initialiser le membre s'il veut posséder la propriété.

C'est tout.

si vous passez par rvalue reference, il n'invoquera qu'une seule instruction" move", mais si pass by value, c'est deux.

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

si vous écrivez une bibliothèque, maintenant vous savez que vous devez La garantir, alors faites-le, passer par 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

0
répondu merito 2018-05-11 03:35:27