381 votes

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

Je suis nouveau dans la sémantique des déplacements en C++11 et je ne sais pas très bien comment gérer unique_ptr les paramètres dans les constructeurs ou les fonctions. Considérons cette classe qui se réfère 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 les fonctions en prenant unique_ptr des arguments ?

Et est-ce que je dois 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?

806voto

Nicol Bolas Points 133791

Voici les manières 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 cette fonction, 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 transfert de la propriété du pointeur vers la fonction/objet/etc en question. Après newBase est construit, nextBase est garanti vide . Vous ne possédez pas l'objet, et vous n'avez même plus de pointeur sur lui. Il n'existe plus.

Ceci est assuré car nous prenons le paramètre par valeur. std::move n'a pas vraiment déplacer quoi que ce soit ; c'est juste un casting fantaisiste. std::move(nextBase) renvoie un Base&& qui est une référence de valeur r à 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 r-value, le C++ construira automatiquement un temporaire pour nous. Il crée un std::unique_ptr<Base> de la Base&& que nous avons donné à la fonction via std::move(nextBase) . C'est la construction de ce temporaire qui en fait se déplace la valeur de nextBase dans l'argument de la fonction n .

(B) Par référence à la valeur non-constante l

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

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

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

La signification est la même que celle de toute autre utilisation des références non-const : la fonction peut ou peut ne pas l'être revendiquent la propriété du pointeur. Étant donné ce code :

Base newBase(nextBase);

Il n'y a aucune garantie que nextBase est vide. Il mai être vide ; il peut ne pas l'être. Cela dépend vraiment de ce que Base::Base(std::unique_ptr<Base> &n) veut faire. Pour cette raison, il n'est pas très évident de savoir ce qui va se passer à partir de la signature de la fonction ; il faut lire l'implémentation (ou la documentation associée).

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

(C) Par référence à la valeur constante l

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

Je ne montre pas d'implémentation, parce que vous no puede passer d'un const& . En passant un const& vous dites que la fonction peut accéder à l'objet de l'enquête. Base via le pointeur, mais il ne peut pas magasin n'importe où. Il ne peut pas en revendiquer la propriété.

Cela peut être utile. Pas nécessairement pour votre cas spécifique, mais il est toujours bon de pouvoir donner un pointeur à quelqu'un et de savoir qu'il no puede (sans enfreindre les règles du C++, comme l'interdiction de jeter des déchets. const ) en revendiquent la propriété. Ils ne peuvent pas le stocker. Ils peuvent la transmettre à d'autres, mais ces derniers doivent respecter les mêmes règles.

(D) Par référence à la valeur r

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

Ce cas est plus ou moins identique à celui de "by non-const l-value reference". Les différences sont de deux ordres.

  1. Vous puede passer un temporaire :

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Vous doit utiliser std::move lors du passage d'arguments non temporaires.

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

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

Vous avez une attente raisonnable que, après que cette ligne soit terminée, nextBase doit être vide. Il devrait avoir été déplacé. Après tout, vous avez ce std::move assis là, vous disant que le mouvement a eu lieu.

Le problème est qu'il ne l'a pas fait. Ce n'est pas garanti d'avoir été déplacé. C'est mai ont été déplacés, mais vous ne le saurez qu'en regardant le code source. La signature de la fonction ne suffit pas pour le savoir.

Recommandations

  • (A) Par valeur : Si vous voulez qu'une fonction prétende propriété d'un unique_ptr , prenez-le par valeur.
  • (C) Par référence à la valeur l const : Si vous voulez qu'une fonction utilise simplement la fonction unique_ptr pour la durée d'exécution de cette fonction, prenez-la par const& . Vous pouvez également passer un & o const& au type réel pointé, plutôt que d'utiliser une balise unique_ptr .
  • (D) Par référence à la valeur r : Si une fonction peut ou non revendiquer la propriété (selon les chemins de code internes), alors prenez-la par && . Mais je vous déconseille fortement de le faire dans la mesure du possible.

Comment manipuler un unique_ptr

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

Si vous prenez un unique_ptr par valeur, vous pouvez en sortir librement. Mais le mouvement ne se produit pas réellement à cause std::move . Prenez l'énoncé suivant :

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

Il s'agit en fait de deux déclarations :

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

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

El temporary est juste une référence à la valeur r de oldPtr . Il se trouve dans le Constructeur de newPtr où le mouvement se produit. unique_ptr (un constructeur qui prend une valeur de && à lui-même) est ce qui fait le mouvement réel.

Si vous avez un unique_ptr et que vous souhaitez la stocker quelque part, vous doit utiliser std::move pour faire le stockage.

64voto

Marc van Leeuwen Points 803

Je vais 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 de l'outil de gestion de la mémoire. std::unique_ptr Il s'applique également à l'ancien modèle de classe de la Commission européenne. std::auto_ptr (qui, selon moi, permet toutes les utilisations du pointeur unique, mais pour lequel, en outre, les valeurs l modifiables seront acceptées là où les valeurs r sont attendues, sans avoir à invoquer le modèle de classe std::move ), et dans une certaine mesure aussi à std::shared_ptr .

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

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

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

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 (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 o shared_ptr à la place, sans qu'il soit nécessaire de changer grand chose à ce qui est dit ci-dessous (notamment en ce qui concerne la sécurité des exceptions qui est assurée sans avoir besoin d'écrire des destructeurs).

Modes de transmission des pointeurs intelligents

Mode 0 : passer un pointeur ou une référence en argument 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 se soucier de la propriété. qui est propriétaire de l'objet pointé, ou par quel moyen cette propriété est gérée, donc passer un pointeur brut est à la fois parfaitement sûr, et la forme la plus flexible, puisque indépendamment de la propriété, un client peut toujours produire un pointeur brut (soit en appelant la fonction get ou de l'opérateur d'adresse & ).

Par exemple, la fonction permettant de calculer la longueur d'une telle liste ne devrait pas recevoir un nom de fichier de type 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 une node n représentant une liste non vide peut appeler length(&n) .

Si le pointeur est garanti 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 ou d'une référence à un objet non nul. const si la fonction doit mettre à jour le contenu du ou des nœuds, sans en ajouter ou en supprimer (ce qui impliquerait la propriété).

Un cas intéressant qui tombe dans la catégorie du mode 0 est la réalisation d'une copie (profonde) de la liste ; alors qu'une fonction faisant cela doit bien sûr transférer la propriété de la copie qu'elle crée, elle n'est pas concernée par la propriété de la liste qu'elle copie. On pourrait donc la définir 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 à copy dans la liste d'initialisation se lie à l'argument de référence rvalue dans le constructeur move de unique_ptr<node> aussi connu sous le nom de list lors de l'initialisation de la next du champ généré node ), et pour la question de savoir pourquoi elle est à l'abri des exceptions (si, au cours du processus d'allocation récursive, la mémoire vient à manquer et qu'un appel de la commande new jette std::bad_alloc alors, à ce moment-là, un pointeur vers la liste partiellement construite est conservé anonymement dans un fichier temporaire de type list créé pour la liste d'initialisation, et son destructeur nettoiera cette liste partielle). Au fait, il faut résister à la tentation de remplacer (comme je l'ai fait initialement) le deuxième élément de la liste des initialisateurs par un élément de la liste des initialisateurs. nullptr por p qui, après tout, est connu pour être nul à ce moment-là : on ne peut pas construire un pointeur intelligent à partir d'un pointeur (brut). à la constante 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 un temporaire anonyme) 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 peut avoir été élidé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 d'avance pour le service appelé, 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 enveloppe 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 un temporaire, laissant la variable nulle.

Pour les cas où la fonction appelée prend inconditionnellement la propriété de l'objet pointé (pilfers), ce mode est utilisé avec std::unique_ptr o std::auto_ptr est un bon moyen de transmettre 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 n'est pas à préférer (un tant soit peu) au mode 1. Pour cette raison, je ne fournirai aucun exemple d'utilisation de ce mode. (Mais voyez le reversed exemple du mode 3 ci-dessous, où l'on remarque que le mode 1 ferait au moins aussi bien l'affaire). Si la fonction prend plus d'arguments que juste ce pointeur, il peut arriver qu'il y ait en plus un pointeur raison technique pour éviter le mode 1 (avec std::unique_ptr o std::auto_ptr ) : puisqu'une opération de déplacement réelle a lieu lors du passage d'une variable pointeur p par l'expression std::move(p) on ne peut pas supposer que p conserve une valeur utile pendant qu'il évalue les autres arguments (l'ordre d'évaluation n'étant pas spécifié), ce qui pourrait conduire à des erreurs subtiles ; en revanche, l'utilisation du mode 3 garantit qu'aucun déplacement de la fonction p a lieu avant l'appel de la fonction, de sorte que les autres arguments peuvent accéder en toute sécurité à une valeur par le biais de la fonction p .

Lorsqu'il est utilisé avec std::shared_ptr ce mode est intéressant dans la mesure où, avec une seule définition de fonction, il permet à l'appelant de choisissez s'il faut conserver une copie partagée du pointeur pour lui-même tout en créant une nouvelle copie partagée qui sera utilisée par la fonction (ceci se produit lorsqu'un argument lvalue est fourni ; le constructeur de copie pour les pointeurs partagés utilisés à l'appel augmente le nombre de références), ou s'il faut simplement donner à la fonction une copie du pointeur sans en conserver une ni toucher au nombre de références (ceci se produit lorsqu'un argument rvalue est fourni, éventuellement un lvalue enveloppé 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 réalisée en définissant séparément void f(const std::shared_ptr<X>& x) (pour le cas de la lvalue) et void f(std::shared_ptr<X>&& x) (pour le cas de rvalue), les corps de fonction ne différant que par le fait que la première version invoque la sémantique de la copie (en utilisant la construction/affectation de la copie lorsque l'on utilise la fonction x ) mais la deuxième version déplace la sémantique (écriture std::move(x) à la place, comme dans l'exemple de code). 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 référence à une valeur l (modifiable)

Ici, la fonction demande simplement d'avoir une référence modifiable au pointeur intelligent, mais ne donne aucune indication sur ce qu'elle va en faire. 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 puede peut être utilisé pour prendre possession de 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 passer à une telle fonction serait obligé de la stocker dans une variable nommée pour pouvoir faire l'appel, puisque le langage ne fournit qu'une conversion implicite vers une variable rvalue. constant référence à une lvalue (se référant à un temporaire) à partir d'une rvalue. (Contrairement à la situation inverse gérée par std::move , un casting de Y&& a Y& avec Y le type de pointeur intelligent, n'est pas possible ; néanmoins, cette conversion pourrait être obtenue par une simple fonction de modèle si on le souhaitait vraiment ; cf. https://stackoverflow.com/a/24868376/1436796 ). Pour le cas où la fonction appelée a l'intention de prendre inconditionnellement possession de l'objet, en volant l'argument, l'obligation de fournir un argument lvalue donne un mauvais signal : la variable n'aura aucune valeur utile après l'appel. C'est pourquoi le mode 3, qui donne des possibilités identiques à l'intérieur de notre fonction mais demande aux appelants de fournir une rvalue, doit ê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 list fournit un exemple d'une telle 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 puisque leur pointeur intelligent possède toujours une liste bien définie et non vide après l'appel, bien qu'une liste différente de celle d'avant.

Encore une fois, il est intéressant d'observer ce qui se passe si la prepend L'appel échoue par manque de mémoire libre. Ensuite, le new l'appel lancera std::bad_alloc à l'heure actuelle, puisqu'aucune node a pu être allouée, il est certain que la référence de la valeur r (mode 3) passée de std::move(l) ne peut pas encore avoir été chapardé, car cela serait fait pour construire la next du champ de la node qui n'ont pas été attribués. Donc le pointeur intelligent original l contient toujours la liste originale lorsque l'erreur est levée ; cette liste sera soit correctement détruite par le destructeur de pointeur intelligent, soit, dans le cas où l devrait survivre grâce à une mise en place suffisamment précoce catch la liste d'origine sera conservée.

C'était un exemple constructif, avec un clin d'œil à cette question on peut également donner l'exemple plus destructif de la 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 l'énoncé final, le pointeur (*p)->next détenu à l'intérieur du nœud à supprimer est délié (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 le noeud p ), en veillant à ce qu'un et seulement un est détruit à ce moment-là. (Dans la forme alternative mentionnée dans le commentaire, cette synchronisation serait laissée aux internes de l'implémentation de l'opérateur de déplacement-affectation de la fonction std::unique_ptr instance list ; la norme dit 20.7.1.2.3;2 que cet opérateur doit agir "comme si en appelant reset(u.release()) ", d'où le timing devrait être sûr ici aussi).

Notez que prepend y remove_first ne peut pas être appelé par les clients qui stockent un node pour une liste toujours non vide, et ce à juste titre puisque les implémentations données ne pouvaient pas fonctionner pour de tels cas.

Mode 3 : passage d'un pointeur intelligent par une référence rvalue (modifiable)

C'est le mode à utiliser de préférence lorsqu'il s'agit simplement de prendre 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 effectivement le pointeur (exactement comme elle le ferait en utilisant le mode 2). La "signature du chèque" signifie concrètement que les appelants doivent emballer un argument dans std::move (comme dans le mode 1) s'il s'agit d'une lvalue (si c'est une rvalue, 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, donc la fonction appelée ne doit pas assumer la propriété ; cependant, j'insiste sur le fait qu'en cas d'incertitude quant au transfert de propriété (dans le cadre d'une utilisation normale), le mode 2 devrait être préféré au mode 3, de sorte que l'utilisation du mode 3 signale implicitement aux appelants qu'ils sont renoncer à la propriété. On pourrait rétorquer que seul le passage d'arguments en mode 1 signale vraiment une perte forcée de propriété aux appelants. Mais si un client a le moindre doute 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 list qui utilise le passage d'arguments en mode 3. Déplacement d'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 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'argument en mode 3 est le suivant qui prend une liste (et sa propriété), et retourne 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 aussi être utilisée différemment.

Ici, l'argument est immédiatement déplacé vers une variable locale pour des raisons 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) ; donc la différence avec le mode 1 de passage d'argument est minime. En fait, en utilisant ce mode, l'argument aurait pu servir directement de variable locale, évitant ainsi ce déplacement initial ; c'est juste un exemple du principe général selon lequel si un argument passé par référence ne sert qu'à initialiser une variable locale, on peut tout aussi bien le passer par valeur à la place 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 utilisent le mode 3. Un cas particulièrement convaincant est celui du constructeur std::shared_ptr<T>(auto_ptr<T>&& p) . Ce constructeur utilisé (dans std::tr1 ) pour prendre un lvalue (tout comme la référence auto_ptr<T>& ), et pourrait donc être appelé avec un constructeur de copie auto_ptr<T> lvalue p dans le cas de std::shared_ptr<T> q(p) après quoi p a été remis à zéro. En raison du passage du mode 2 au mode 3 pour le passage des arguments, cet ancien code doit maintenant être réécrit en std::shared_ptr<T> q(std::move(p)) et continuera ensuite à travailler. Je comprends que le comité n'ait pas aimé le mode 2 ici, mais il avait la possibilité de passer au mode 1, en définissant std::shared_ptr<T>(auto_ptr<T> p) Au lieu de cela, ils auraient pu s'assurer que l'ancien code fonctionne sans modification, car (contrairement aux pointeurs uniques) les auto-pointeurs peuvent être déréférencés silencieusement vers 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à 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ù la prise en charge de la propriété prendrait autrement la forme d'un déplacement du pointeur intelligent vers une variable locale, comme dans l'exemple de l'article reversed exemple ci-dessus. Cependant, je vois 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 pointeur temporaire et de supprimer l'ancien pointeur (la gestion de l'argent liquide est quelque peu laborieuse) ; dans certains scénarios, le pointeur peut être passé plusieurs fois inchangé à une autre fonction avant d'être réellement chapardé. Un tel passage nécessitera généralement d'écrire std::move (à moins que le mode 2 ne soit utilisé), mais notez qu'il s'agit juste d'un cast qui ne fait rien (en particulier pas de déréférencement), donc il n'a aucun coût attaché.

  • S'il est concevable que quelque chose lève une exception entre le début de l'appel de la fonction et le moment où celle-ci (ou un appel contenu) déplace effectivement l'objet pointé dans une autre structure de données (et que cette exception n'est pas déjà prise en compte dans la fonction elle-même), alors, en utilisant le mode 1, l'objet auquel se réfère le pointeur intelligent sera détruit avant qu'une fonction catch clause peut gérer l'exception (parce que le paramètre de la fonction a été détruit pendant le déroulement de la pile), mais pas lorsqu'on utilise le mode 3. Ce dernier donne à l'appelant la possibilité de récupérer les données de l'objet dans ce cas (en attrapant l'exception). Notez que le mode 1 ici ne provoque pas de fuite de mémoire mais peut conduire à une perte irrécupérable de données pour le programme, ce qui peut également être indésirable.

Retourner un pointeur intelligent : toujours par valeur

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

4voto

Xeo Points 69818

Oui, vous devez le faire si vous prenez le unique_ptr par valeur dans le constructeur. L'explicitation est une bonne chose. Puisque unique_ptr n'est pas copiable (private copy ctor), ce que vous avez écrit devrait vous donner une erreur de compilation.

3voto

Omnifarious Points 25666

Editar: Cette réponse est fausse, même si, à proprement parler, le code fonctionne. Je ne la laisse ici que parce que la discussion qui s'y rapporte est trop utile. Cette autre réponse est la meilleure réponse donnée au moment de la dernière édition : 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 exprimer le fait qu'ils connaissent le unique_ptr dans lequel ils passent perdront la propriété.

Cela signifie que vous devez utiliser une référence rvalue à un fichier unique_ptr dans vos méthodes, et non un unique_ptr lui-même. Cela ne fonctionnera pas de toute façon, car le fait de passer dans un simple ancien unique_ptr nécessiterait de faire une copie, et c'est explicitement interdit dans l'interface de unique_ptr . Il est intéressant de noter que l'utilisation d'une référence à une rvalue nommée la transforme à nouveau en lvalue. ::std::move à l'intérieur de vos méthodes également.

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 personnes utilisant ces méthodes le feraient alors :

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 le fait que le pointeur va perdre sa propriété au moment où il est le plus pertinent et utile de le savoir. Si cela se passait de manière invisible, il serait très déroutant pour les personnes utilisant votre classe d'avoir objptr perdent soudainement leur propriété sans raison apparente.

3voto

einpoklum Points 2893

tl;dr : N'utilisez pas unique_ptr C'est comme ça.

Je crois que vous êtes en train de créer un terrible désordre - pour ceux qui devront lire votre code, le maintenir, et probablement ceux qui devront l'utiliser.

Prenez seulement unique_ptr si vous avez des paramètres de constructeur exposés publiquement unique_ptr membres.

unique_ptr pour la gestion de la propriété et de la durée de vie des pointeurs bruts. Ils sont parfaits pour une utilisation localisée, mais pas pour l'interfaçage, qui n'est d'ailleurs pas prévu. Vous voulez vous interfacer ? Documentez votre nouvelle classe comme prenant la propriété, et laissez-la obtenir la ressource brute ; ou peut-être, dans le cas de pointeurs, utilisez owner<T*> comme suggéré dans le Directives de base .

Seulement si le but de votre cours est de tenir unique_ptr et que d'autres utilisent ces unique_ptr en tant que tel - ce n'est que dans ce cas qu'il est raisonnable pour votre constructeur ou vos méthodes de les prendre.

N'exposez pas le fait que vous utilisez unique_ptr en interne.

Utilisation de unique_ptr pour les nœuds de liste est un détail d'implémentation. En fait, même le fait que vous laissiez les utilisateurs de votre mécanisme de type liste utiliser directement le nœud de liste nu - en le construisant eux-mêmes et en vous le donnant - n'est pas une bonne idée IMHO. Je ne devrais pas avoir besoin de former un nouveau noeud de liste-qui-est-aussi-une-liste pour ajouter quelque chose à votre liste - je devrais juste passer la charge utile - par valeur, par const lvalue ref et/ou par rvalue ref. Ensuite, il faut s'en occuper. Et pour épisser les listes - encore une fois, par valeur, const lvalue et/ou rvalue.

Prograide.com

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

Powered by:

X