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_ptr
paramè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_ptr
arguments?
Et dois-je utiliser std::move
dans le code appelant?
Base::UPtr b1;
Base::UPtr b2(new Base());
b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
c++
arguments
c++11
unique-ptr
codablank1
la source
la source
Réponses:
Voici les façons possibles de prendre un pointeur unique comme argument, ainsi que leur signification associée.
(A) Par valeur
Pour que l'utilisateur puisse appeler cela, il doit effectuer l'une des opérations suivantes:
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
newBase
construit,nextBase
est 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::move
ne bouge en fait rien; c'est juste un casting de fantaisie.std::move(nextBase)
renvoie unBase&&
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 duBase&&
que nous avons donné la fonction viastd::move(nextBase)
. C'est la construction de ce temporaire qui déplace réellement la valeur denextBase
dans l'argument de fonctionn
.(B) Par référence de valeur l non constante
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:
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:
Il n'y a aucune garantie
nextBase
vide. Il peut être vide; il se peut que non. Cela dépend vraiment de ceBase::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
Je ne montre pas d'implémentation, car vous ne pouvez pas passer d'un
const&
. En passant aconst&
, vous dites que la fonction peut accéder auBase
via 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
Ceci est plus ou moins identique au cas "par référence de valeur l non constante". Les différences sont deux choses.
Vous pouvez passer un temporaire:
Vous devez utiliser
std::move
lors du passage d'arguments non temporaires.Ce dernier est vraiment le problème. Si vous voyez cette ligne:
Vous avez une attente raisonnable qui, une fois cette ligne terminée,
nextBase
doit ê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
unique_ptr
, prenez-la par valeur.unique_ptr
pour la durée de l'exécution de cette fonction, prenez-laconst&
. Vous pouvez également passer un&
ouconst&
au type réel pointé, plutôt que d'utiliser ununique_ptr
.&&
. 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::move
utiliser la fonction de bibliothèque standard.Si vous prenez une
unique_ptr
valeur par, vous pouvez vous en déplacer librement. Mais le mouvement ne se produit pas réellement à cause destd::move
. Prenez la déclaration suivante:Il s'agit en réalité de deux déclarations:
(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
temporary
est juste une référence de valeur r àoldPtr
. C'est dans le constructeur de l'newPtr
endroit où le mouvement se produit.unique_ptr
Le 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_ptr
valeur et que vous souhaitez la stocker quelque part, vous devez utiliserstd::move
pour effectuer le stockage.la source
std::move
ne nomme pas sa valeur de retour. N'oubliez pas que les références rvalue nommées sont des lvalues. ideone.com/VlEM3unique_ptr
; peut-être que certains autres appelants ont besoin de la même fonctionnalité mais tiennent unshared_ptr
appel à 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.unique_ptr
valeurs par la référence rvalue (par exemple lors de leur transformationshared_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éesstd::move
, mais pas des valeurs nues).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_ptr
modèle de classe; il s'applique également à l'ancienstd::auto_ptr
modè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 à invoquerstd::move
), et dans une certaine mesure égalementstd::shared_ptr
.Comme exemple concret pour la discussion, je considérerai le type de liste simple suivant
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
list
pointeur initial . Si le code client sait que la liste qu'il stocke ne sera jamais vide, il peut également choisir de stocker la premièrenode
directement plutôt que alist
. Aucun destructeur pour lesnode
besoins à 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
list
est bien sûr biaiséunique_ptr
, mais la définition pourrait être modifiée pour être utiliséeauto_ptr
ou à lashared_ptr
place 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
get
mé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
list
argument, mais un pointeur brut:Un client qui contient une variable
list head
peut appeler cette fonction commelength(head.get())
, tandis qu'un client qui a choisi de stocker unenode n
liste non vide peut appelerlength(&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-
const
si 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:
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 de déplacement deunique_ptr<node>
, akalist
, lors de l'initialisation dunext
champ 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 denew
lancersstd::bad_alloc
, alors à ce moment-là, un pointeur vers la liste partiellement construite est conservé de manière anonyme dans un temporaire de typelist
créé 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 secondnullptr
parp
, 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 dans
std::move
si 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_ptr
oustd::auto_ptr
est 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'reversed
exemple 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 (avecstd::unique_ptr
oustd::auto_ptr
): puisqu'une opération de déplacement réelle a lieu lors du passage d'une variable de pointeurp
par 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 quep
p
p
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 destd::move
). Par exempleLa même chose pourrait être obtenue en définissant séparément
void f(const std::shared_ptr<X>& x)
(pour le cas lvalue) etvoid 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'utilisationx
) mais la deuxième version déplace la sémantique (en écrivant à lastd::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 deY&&
àY&
, avecY
le 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
list
fournit un exemple d'une telle utilisation: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'
prepend
appel échoue par manque de mémoire libre. Ensuite, l'new
appel sera lancéstd::bad_alloc
; à ce stade, comme aucunnode
n'a pu être alloué, il est certain que la référence de valeur r passée (mode 3) destd::move(l)
ne peut pas encore être volée, car cela serait fait pour construire lenext
champ de celuinode
qui n'a pas été alloué. Ainsi, le pointeur intelligent d'originel
contient 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ùl
devrait survivre grâce à unecatch
clause 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:
Encore une fois, l'exactitude est assez subtile ici. Notamment, dans la déclaration finale, le pointeur
(*p)->next
détenu à l'intérieur du nœud à supprimer n'est pas lié (parrelease
, qui renvoie le pointeur mais rend la valeur nulle d'origine) avantreset
(implicitement) de détruire ce nœud (lorsqu'il détruit l'ancienne valeur détenue parp
), 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_ptr
instancelist
; la norme dit 20.7.1.2.3; 2 que cet opérateur devrait agir "comme si par appelerreset(u.release())
", d'où le moment devrait être sûr ici aussi.)Notez que
prepend
etremove_first
ne peuvent pas être appelés par les clients qui stockent unenode
variable 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
list
type qui utilise le passage d'arguments en mode 3. Déplacer une listeb
à la fin d'une autre listea
est un exemple typique; cependanta
(qui survit et détient le résultat de l'opération) est mieux passé en utilisant le mode 2: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.
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
l
directement à la place dep
, 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 (instd::tr1
) pour prendre une référence lvalue modifiable (tout comme leauto_ptr<T>&
constructeur de copie), et pouvait donc être appelé avec uneauto_ptr<T>
lvaluep
comme instd::shared_ptr<T> q(p)
, après quoi ilp
a é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'
reversed
exemple 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
catch
clause 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'return
instruction). Personne ne veut obtenir une référence à un pointeur qui vient probablement d'être supprimé.la source
unique_ptr
ait é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é.unique_ptr
pré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.Oui, vous devez le faire si vous prenez la
unique_ptr
valeur par dans le constructeur. L'explicité est une bonne chose. Comme ilunique_ptr
est non copiable (ctor de copie privée), ce que vous avez écrit devrait vous donner une erreur de compilation.la source
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::move
est que les gens qui vous dépassentunique_ptr
devraient l'utiliser pour exprimer la connaissance qu'ils saventunique_ptr
qu'ils vont perdre la propriété.Cela signifie que vous devez utiliser une référence rvalue à un
unique_ptr
dans vos méthodes, pas ununique_ptr
lui - même. Cela ne fonctionnera pas de toute façon, car le passage dans un ancien simpleunique_ptr
nécessiterait de faire une copie, ce qui est explicitement interdit dans l'interface deunique_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:
Ensuite, les personnes utilisant les méthodes le feraient:
Comme vous le voyez, le
::std::move
exprime 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'avoirobjptr
soudainement perdu la propriété sans raison apparente.la source
Base fred(::std::move(objptr));
et nonBase::UPtr fred(::std::move(objptr));
?std::move
dans l'implémentation du constructeur et de la méthode. Et même lorsque vous passez par valeur, l'appelant doit toujours utiliserstd::move
pour 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.devrait être beaucoup mieux que
et
devrait être
avec le même corps.
Et ... ce qui est
evt
enhandle()
??la source
std::forward
ici:Base::UPtr&&
est toujours un type de référence rvalue, et lestd::move
passe comme rvalue. Il est déjà transmis correctement.unique_ptr
valeur 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 launique_ptr
variable 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'utilisateurstd::move
suspecte et déroutante. L'utilisationstd::move
doit toujours garantir que quelque chose a été déplacé .À 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:
Je dois écrire du code
Base newBase(std::move(<lvalue>))
ouBase newBase(<rvalue>)
.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
la source