Come faccio a passare un argomento unique_ptr a un costruttore o a una funzione?


400

Sono nuovo di spostare la semantica in C ++ 11 e non so molto bene come gestire i unique_ptrparametri nei costruttori o nelle funzioni. Considera questa classe che fa riferimento a se stessa:

#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;

};

È così che dovrei scrivere funzioni prendendo unique_ptrargomenti?

E devo usare std::moveil codice chiamante?

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

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


1
Non è un errore di segmentazione mentre chiami b1-> setNext su un puntatore vuoto?
Balki,

Risposte:


836

Ecco i modi possibili per prendere un puntatore univoco come argomento, nonché il significato associato.

(A) Per valore

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

Affinché l'utente possa chiamarlo, deve effettuare una delle seguenti operazioni:

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

Prendere un puntatore univoco per valore significa che si sta trasferendo la proprietà del puntatore alla funzione / oggetto / ecc. In questione. Dopo che newBaseè stato costruito, nextBaseè garantito per essere vuoto . Non possiedi l'oggetto e non hai nemmeno più un puntatore ad esso. È andato.

Questo è garantito perché prendiamo il parametro per valore. std::movein realtà non sposta nulla; è solo un cast di fantasia. std::move(nextBase)restituisce a Base&&che è un riferimento di valore r nextBase. Questo è tutto.

Poiché Base::Base(std::unique_ptr<Base> n)prende il suo argomento in base al valore anziché al riferimento al valore r, C ++ costruirà automaticamente un temporaneo per noi. Crea un std::unique_ptr<Base>da Base&&cui abbiamo assegnato la funzione std::move(nextBase). È la costruzione di questo temporaneo che sposta effettivamente il valore nextBasedall'argomento della funzione n.

(B) Per riferimento a valore l non costante

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

Questo deve essere chiamato su un valore l reale (una variabile denominata). Non può essere chiamato con un temporaneo come questo:

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

Il significato di questo è lo stesso di qualsiasi altro uso di riferimenti non costanti: la funzione può o meno rivendicare la proprietà del puntatore. Dato questo codice:

Base newBase(nextBase);

Non vi è alcuna garanzia che nextBasesia vuota. Esso può essere vuoto; non può. Dipende davvero da cosa Base::Base(std::unique_ptr<Base> &n)vuole fare. Per questo motivo, non è molto evidente solo dalla firma della funzione cosa accadrà; devi leggere l'implementazione (o la documentazione associata).

Per questo motivo, non lo suggerirei come interfaccia.

(C) Per riferimento valore costante l

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

Non mostro un'implementazione, perché non puoi spostarti da a const&. Passando a const&, stai dicendo che la funzione può accedere Basetramite il puntatore, ma non può salvarla da nessuna parte. Non può rivendicarne la proprietà.

Questo può essere utile Non necessariamente per il tuo caso specifico, ma è sempre bene essere in grado di consegnare a qualcuno un puntatore e sapere che non possono (senza infrangere le regole del C ++, come non rifiutare const) di rivendicarne la proprietà. Non possono memorizzarlo. Possono passarlo ad altri, ma quegli altri devono attenersi alle stesse regole.

(D) Per riferimento valore r

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

Questo è più o meno identico al caso "per riferimento a valore l non costante". Le differenze sono due cose.

  1. È possibile passare una temporanea:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. È necessario utilizzare std::movequando si passano argomenti non temporanei.

Quest'ultimo è davvero il problema. Se vedi questa riga:

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

Hai una ragionevole aspettativa che, una volta completata questa riga, nextBasedovrebbe essere vuota. Dovrebbe essere stato spostato da. Dopotutto, hai quel std::moveseduto lì, che ti dice che si è verificato un movimento.

Il problema è che non è così. Non è garantito che sia stato spostato da. Si potrebbe essere stato spostato da, ma si sa solo guardando il codice sorgente. Non si può dire solo dalla firma della funzione.

raccomandazioni

  • (A) Per valore: se intendi che una funzione richieda la proprietà di a unique_ptr, prendila per valore.
  • (C) Per riferimento a valore l costante: Se intendi che una funzione usi semplicemente la unique_ptrper la durata dell'esecuzione di quella funzione, prendila const&. In alternativa, passa a &o const&al tipo effettivo indicato, anziché utilizzare a unique_ptr.
  • (D) Per riferimento valore r: Se una funzione può o non può rivendicare la proprietà (a seconda dei percorsi di codice interni), prendila &&. Ma sconsiglio vivamente di farlo quando possibile.

Come manipolare unique_ptr

Non è possibile copiare a unique_ptr. Puoi solo spostarlo. Il modo corretto per farlo è con la std::movefunzione di libreria standard.

Se prendi un unique_ptrvalore, puoi spostarti da esso liberamente. Ma il movimento in realtà non avviene a causa di std::move. Prendi la seguente dichiarazione:

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

Sono davvero due affermazioni:

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

(nota: il codice sopra riportato non viene compilato tecnicamente, poiché i riferimenti di valore r non temporanei non sono in realtà valori r. È qui solo a scopo dimostrativo).

Il temporaryè solo un riferimento r-valore oldPtr. È nel costruttore di newPtrdove avviene il movimento. unique_ptrIl costruttore di mosse (un costruttore che prende &&a se stesso) è ciò che fa il movimento reale.

Se si dispone di un unique_ptrvalore e si desidera archiviarlo da qualche parte, è necessario utilizzare std::moveper eseguire l'archiviazione.


5
@Nicol: ma std::movenon nomina il suo valore di ritorno. Ricorda che i riferimenti ai valori nominali sono valori. ideone.com/VlEM3
R. Martinho Fernandes

31
Sono sostanzialmente d'accordo con questa risposta, ma ho alcune osservazioni. (1) Non penso che ci sia un caso d'uso valido per passare il riferimento al valore costante: tutto ciò che la chiamata potrebbe fare con quello, può fare anche con riferimento al puntatore const (nudo), o anche meglio il puntatore stesso [e non è affar suo sapere che la proprietà è detenuta in a unique_ptr; forse alcuni altri chiamanti necessitano della stessa funzionalità ma tengono shared_ptrinvece una ]] (2) chiamata per riferimento lvalue potrebbe essere utile se chiamata funzione modifica il puntatore, ad esempio aggiungendo o rimuovendo nodi (di proprietà dell'elenco) da un elenco collegato.
Marc van Leeuwen,

8
... (3) Sebbene l'argomento che favorisce il passaggio per valore rispetto al passaggio per riferimento rvalore abbia senso, penso che lo standard stesso passi sempre unique_ptrvalori per riferimento rvalore (ad esempio quando li trasforma in shared_ptr). La logica di ciò potrebbe essere che è leggermente più efficiente (non si passa a puntatori temporanei) mentre fornisce esattamente gli stessi diritti al chiamante (può passare valori o valori racchiusi std::move, ma non valori nudi).
Marc van Leeuwen,

19
Solo per ripetere ciò che Marc ha detto, e citando Sutter : "Non usare una const unique_ptr & come parametro; usa invece il widget *"
Jon

17
Abbiamo scoperto un problema in base al valore : lo spostamento avviene durante l'inizializzazione dell'argomento, che non è ordinata rispetto ad altre valutazioni degli argomenti (tranne ovviamente in una lista_inizializzatore, ovviamente). Considerando che accettare un riferimento al valore, si ordina fortemente che si verifichi lo spostamento dopo la chiamata della funzione e quindi dopo la valutazione di altri argomenti. Pertanto, l'accettazione del riferimento al valore dovrebbe essere preferita ogni volta che verrà assunta la proprietà.
Ben Voigt,

57

Vorrei provare a indicare le diverse modalità praticabili per passare i puntatori agli oggetti la cui memoria è gestita da un'istanza del std::unique_ptrmodello di classe; si applica anche al std::auto_ptrmodello di classe precedente (che credo consenta a tutti gli usi che fa quel puntatore univoco, ma per il quale saranno accettati anche valori modificabili dove sono previsti i valori, senza dover invocare std::move), e in una certa misura anche a std::shared_ptr.

Come esempio concreto per la discussione, prenderò in considerazione il seguente tipo di elenco semplice

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

Le istanze di tale elenco (a cui non è consentito condividere parti con altre istanze o essere circolari) sono interamente di proprietà di chiunque detenga il listpuntatore iniziale . Se il codice client sa che l'elenco che memorizza non sarà mai vuoto, può anche scegliere di memorizzare il primo nodedirettamente anziché un list. Non è nodenecessario definire alcun distruttore : poiché i distruttori per i suoi campi vengono chiamati automaticamente, l'intero elenco verrà eliminato in modo ricorsivo dal distruttore di puntatori intelligenti una volta terminata la durata del puntatore o nodo iniziale.

Questo tipo ricorsivo offre l'occasione per discutere alcuni casi che sono meno visibili nel caso di un puntatore intelligente a dati semplici. Anche le funzioni stesse forniscono occasionalmente (ricorsivamente) anche un esempio di codice client. Il typedef per listè ovviamente distorto unique_ptr, ma la definizione potrebbe essere cambiata per usare auto_ptro shared_ptrinvece senza molto bisogno di cambiare a quanto detto di seguito (in particolare per quanto riguarda la sicurezza delle eccezioni garantita senza la necessità di scrivere distruttori).

Modalità di passaggio dei puntatori intelligenti

Modalità 0: passa un puntatore o un argomento di riferimento anziché un puntatore intelligente

Se la tua funzione non è interessata alla proprietà, questo è il metodo preferito: non fargli assumere un puntatore intelligente. In questo caso la tua funzione non ha bisogno di preoccuparti di chi possiede l'oggetto puntato, o con quale mezzo la proprietà è gestita, quindi passare un puntatore non elaborato è perfettamente sicuro e la forma più flessibile, dal momento che indipendentemente dalla proprietà un cliente può sempre produce un puntatore non elaborato (chiamando il getmetodo o dall'operatore dell'indirizzo &).

Ad esempio la funzione per calcolare la lunghezza di tale elenco, non dovrebbe essere un listargomento, ma un puntatore non elaborato:

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

Un client che contiene una variabile list headpuò chiamare questa funzione come length(head.get()), mentre un client che ha scelto invece di memorizzare un node nelenco che rappresenta un elenco non vuoto può chiamare length(&n).

Se il puntatore è garantito come non nullo (il che non è il caso qui poiché gli elenchi potrebbero essere vuoti) si potrebbe preferire passare un riferimento anziché un puntatore. Potrebbe essere un puntatore / riferimento a non- constse la funzione deve aggiornare i contenuti dei nodi, senza aggiungerli o rimuoverli (quest'ultimo implicherebbe la proprietà).

Un caso interessante che rientra nella categoria modalità 0 è la creazione di una (profonda) copia dell'elenco; mentre una funzione che fa ciò deve ovviamente trasferire la proprietà della copia che crea, non si preoccupa della proprietà dell'elenco che sta copiando. Quindi potrebbe essere definito come segue:

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

Questo codice merita uno sguardo ravvicinato, sia per la domanda sul perché si compili del tutto (il risultato della chiamata ricorsiva copynell'elenco di inizializzatori si lega all'argomento di riferimento del valore nel costruttore di spostamento di unique_ptr<node>, aka list, quando si inizializza il nextcampo del generato node), e per la domanda sul perché è sicura rispetto alle eccezioni (se durante la memoria ricorsiva processo di allocazione si esaurisce e che alcuni chiamano di newtiri std::bad_alloc, poi in quel momento un puntatore alla lista in parte costruito si svolge in forma anonima in una temporanea di tipo listcreato per l'elenco di inizializzatori e il relativo distruttore pulirà tale elenco parziale). Tra l'altro si dovrebbe resistere alla tentazione di sostituire (come ho inizialmente fatto) il secondo nullptrdap, che dopo tutto è noto per essere nullo in quel punto: non è possibile costruire un puntatore intelligente da un puntatore (grezzo) a costante , anche quando è noto che è nullo.

Modalità 1: passa un puntatore intelligente per valore

Una funzione che accetta un valore di puntatore intelligente come argomento prende possesso dell'oggetto immediatamente indicato: il puntatore intelligente che il chiamante deteneva (sia in una variabile nominata o un temporaneo anonimo) viene copiato nel valore dell'argomento all'ingresso della funzione e il chiamante il puntatore è diventato nullo (nel caso di un temporaneo la copia potrebbe essere stata elusa, ma in ogni caso il chiamante ha perso l'accesso all'oggetto puntato). Vorrei chiamare questa modalità chiamata in contanti : il chiamante paga in anticipo per il servizio chiamato e non può avere illusioni sulla proprietà dopo la chiamata. Per chiarire questo, le regole del linguaggio richiedono che il chiamante includa l'argomentostd::movese il puntatore intelligente è tenuto in una variabile (tecnicamente, se l'argomento è un valore); in questo caso (ma non per la modalità 3 di seguito) questa funzione fa ciò che suggerisce il suo nome, ovvero sposta il valore dalla variabile a una temporanea, lasciando la variabile nulla.

Per i casi in cui la funzione chiamata assume incondizionatamente la proprietà di (pilfers) l'oggetto puntato, questa modalità utilizzata con std::unique_ptro std::auto_ptrè un buon modo per passare un puntatore insieme alla sua proprietà, evitando così qualsiasi rischio di perdite di memoria. Tuttavia, penso che ci siano solo pochissime situazioni in cui la modalità 3 di seguito non deve essere preferita (mai così leggermente) rispetto alla modalità 1. Per questo motivo non fornirò esempi di utilizzo di questa modalità. (Ma vedi l' reversedesempio della modalità 3 di seguito, in cui si osserva che anche la modalità 1 farebbe almeno altrettanto.) Se la funzione accetta più argomenti di questo semplice puntatore, può accadere che ci sia anche un motivo tecnico per evitare la modalità 1 (con std::unique_ptro std::auto_ptr): poiché viene eseguita un'operazione di spostamento effettiva mentre si passa una variabile puntatorepdall'espressione std::move(p), non si può presumere che abbia pun valore utile durante la valutazione degli altri argomenti (l'ordine di valutazione non è specificato), il che potrebbe portare a sottili errori; al contrario, l'utilizzo della modalità 3 assicura che non si verifichino spostamenti pprima della chiamata alla funzione, in modo che altri argomenti possano accedere in modo sicuro a un valore tramite p.

Se utilizzata con std::shared_ptr, questa modalità è interessante in quanto con una singola definizione di funzione consente al chiamante di scegliere se mantenere una copia condivisa del puntatore per sé mentre crea una nuova copia condivisa che deve essere utilizzata dalla funzione (ciò accade quando un valore viene fornito l'argomento; il costruttore della copia per i puntatori condivisi utilizzati durante la chiamata aumenta il conteggio dei riferimenti) o per assegnare alla funzione una copia del puntatore senza trattenerne uno o toccare il conteggio dei riferimenti (ciò accade quando viene fornito un argomento rvalue, possibilmente un valore racchiuso in una chiamata di std::move). Per esempio

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
}

Lo stesso si può ottenere definendo separatamente void f(const std::shared_ptr<X>& x)(per il caso lvalue) e void f(std::shared_ptr<X>&& x)(per il caso rvalue), con corpi funzione diversi solo per il fatto che la prima versione invoca la semantica della copia (usando la costruzione / assegnazione della copia quando si usa x) ma la seconda versione sposta la semantica (scrivendo std::move(x)invece, come nel codice di esempio). Pertanto, per i puntatori condivisi, la modalità 1 può essere utile per evitare la duplicazione del codice.

Modalità 2: passa un puntatore intelligente tramite riferimento al valore (modificabile)

Qui la funzione richiede solo di avere un riferimento modificabile al puntatore intelligente, ma non fornisce indicazioni su cosa ne farà. Vorrei chiamare questo metodo chiamata per carta : il chiamante assicura il pagamento fornendo un numero di carta di credito. Il riferimento può essere utilizzato per diventare proprietario dell'oggetto puntato, ma non è necessario. Questa modalità richiede di fornire un argomento lvalue modificabile, corrispondente al fatto che l'effetto desiderato della funzione può includere lasciare un valore utile nella variabile argomento. Un chiamante con un'espressione rvalore che desidera passare a tale funzione sarebbe costretto a memorizzarlo in una variabile denominata per poter effettuare la chiamata, poiché il linguaggio fornisce solo la conversione implicita in una costanteriferimento al valore (riferito a un temporaneo) da un valore. (A differenza della situazione opposta gestita da std::move, un cast da Y&&a Y&, con Yil tipo di puntatore intelligente, non è possibile; tuttavia questa conversione potrebbe essere ottenuta da una semplice funzione modello se lo si desidera davvero; vedere https://stackoverflow.com/a/24868376 / 1436796 ). Nel caso in cui la funzione chiamata intende assumere incondizionatamente la proprietà dell'oggetto, rubando dall'argomento, l'obbligo di fornire un argomento lvalue sta dando il segnale sbagliato: la variabile non avrà alcun valore utile dopo la chiamata. Pertanto la modalità 3, che offre identiche possibilità all'interno della nostra funzione ma chiede ai chiamanti di fornire un valore, dovrebbe essere preferita per tale utilizzo.

Tuttavia, esiste un caso d'uso valido per la modalità 2, vale a dire le funzioni che possono modificare il puntatore o l'oggetto indicato in un modo che implica la proprietà . Ad esempio, una funzione che antepone un nodo a listfornisce un esempio di tale utilizzo:

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

Chiaramente sarebbe indesiderabile qui forzare l'uso dei chiamanti std::move, poiché il loro puntatore intelligente possiede ancora un elenco ben definito e non vuoto dopo la chiamata, sebbene diverso da prima.

Ancora una volta è interessante osservare cosa succede se la prependchiamata fallisce per mancanza di memoria libera. Quindi la newchiamata verrà lanciata std::bad_alloc; in questo momento, poiché non è nodestato possibile assegnare alcun valore , è certo che il riferimento al valore passato (modalità 3) da std::move(l)non può essere stato ancora rubato, in quanto ciò verrebbe fatto per costruire il nextcampo di quello nodeche non è stato assegnato. Quindi il puntatore intelligente originale lcontiene ancora l'elenco originale quando viene generato l'errore; tale elenco verrà o correttamente distrutto dal distruttore di puntatori intelligenti o, nel caso in cui ldovesse sopravvivere grazie a una catchclausola sufficientemente precoce , conterrà comunque l'elenco originale.

Questo è stato un esempio costruttivo; con un occhiolino a questa domanda si può anche dare l'esempio più distruttivo di rimuovere il primo nodo contenente un dato valore, se presente:

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); 
}

Anche in questo caso la correttezza è abbastanza sottile. In particolare, nell'istruzione finale il puntatore (*p)->nexttenuto all'interno del nodo da rimuovere è scollegato (da release, che restituisce il puntatore ma rende nullo l'originale) prima reset (implicitamente) distrugge quel nodo (quando distrugge il vecchio valore detenuto da p), assicurando che un solo nodo viene distrutto in quel momento. (Nella forma alternativa menzionata nel commento, questa tempistica sarebbe lasciata agli interni dell'implementazione dell'operatore di assegnazione dei movimenti std::unique_ptrdell'istanza list; lo standard dice 20.7.1.2.3; 2 che questo operatore dovrebbe agire "come se chiamando reset(u.release())", da qui anche i tempi dovrebbero essere al sicuro.)

Si noti che prepende remove_firstnon può essere chiamato dai client che memorizzano una nodevariabile locale per un elenco sempre non vuoto, e giustamente poiché le implementazioni fornite non possono funzionare in questi casi.

Modalità 3: passa un puntatore intelligente tramite riferimento al valore (modificabile)

Questa è la modalità preferita da usare quando si diventa semplicemente proprietari del puntatore. Vorrei chiamare questo metodo chiamata tramite assegno : il chiamante deve accettare la rinuncia alla proprietà, come se fornisse denaro, firmando l'assegno, ma l'effettivo prelievo viene posticipato fino a quando la funzione chiamata non impegna effettivamente il puntatore (esattamente come farebbe quando si utilizza la modalità 2 ). La "firma del controllo" significa concretamente che i chiamanti devono avvolgere un argomento std::move(come nella modalità 1) se è un valore (se è un valore, la parte "rinunciare alla proprietà" è ovvia e non richiede un codice separato).

Si noti che tecnicamente la modalità 3 si comporta esattamente come la modalità 2, quindi la funzione chiamata non deve assumere la proprietà; tuttavia vorrei insistere sul fatto che in caso di incertezza sul trasferimento della proprietà (nell'uso normale), la modalità 2 dovrebbe essere preferita alla modalità 3, in modo che l'uso della modalità 3 sia implicitamente un segnale per i chiamanti che stanno cedendo la proprietà. Si potrebbe replicare che solo l'argomento della modalità 1 che passa in realtà segnala la perdita forzata della proprietà ai chiamanti. Ma se un cliente ha dei dubbi sulle intenzioni della funzione chiamata, dovrebbe conoscere le specifiche della funzione chiamata, che dovrebbe rimuovere ogni dubbio.

È sorprendentemente difficile trovare un esempio tipico che coinvolga il nostro listtipo che utilizza il passaggio dell'argomento in modalità 3. Lo spostamento di un elenco balla fine di un altro elenco aè un tipico esempio; tuttavia a(che sopravvive e detiene il risultato dell'operazione) è meglio passare usando la modalità 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 puro esempio di passaggio dell'argomento in modalità 3 è il seguente che prende un elenco (e la sua proprietà) e restituisce un elenco contenente i nodi identici in ordine inverso.

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;
}

Questa funzione può essere chiamata come in l = reversed(std::move(l));per invertire l'elenco in se stesso, ma l'elenco invertito può anche essere utilizzato in modo diverso.

Qui l'argomento viene immediatamente spostato in una variabile locale per efficienza (si potrebbe usare il parametro ldirettamente al posto di p, ma accedervi ogni volta comporterebbe un ulteriore livello di riferimento indiretto); quindi la differenza con il passaggio dell'argomento in modalità 1 è minima. In effetti usando quella modalità, l'argomento avrebbe potuto servire direttamente come variabile locale, evitando così quella mossa iniziale; questa è solo un'istanza del principio generale secondo cui se un argomento passato per riferimento serve solo a inizializzare una variabile locale, si potrebbe anche passarlo per valore e usare il parametro come variabile locale.

L'uso della modalità 3 sembra essere sostenuto dallo standard, come testimonia il fatto che tutte le funzioni di libreria fornite che trasferiscono la proprietà dei puntatori intelligenti utilizzando la modalità 3. Un caso particolare convincente è il costruttore std::shared_ptr<T>(auto_ptr<T>&& p). Quel costruttore ha usato (in std::tr1) per prendere un riferimento al valore modificabile (proprio come il auto_ptr<T>&costruttore della copia), e potrebbe quindi essere chiamato con un auto_ptr<T>valore pcome in std::shared_ptr<T> q(p), dopo di che pè stato resettato a null. A causa del passaggio dalla modalità 2 alla 3 nel passaggio degli argomenti, questo vecchio codice deve ora essere riscritto std::shared_ptr<T> q(std::move(p))e continuerà a funzionare. Capisco che al comitato non piaceva la modalità 2 qui, ma avevano la possibilità di passare alla modalità 1, definendostd::shared_ptr<T>(auto_ptr<T> p)invece, avrebbero potuto assicurarsi che il vecchio codice funzioni senza modifiche, poiché (diversamente dai puntatori univoci) i puntatori automatici possono essere silenziosamente riferiti a un valore (l'oggetto puntatore stesso viene reimpostato su null nel processo). Apparentemente, il comitato ha preferito così tanto sostenere la modalità 3 rispetto alla modalità 1, che hanno scelto di rompere attivamente il codice esistente piuttosto che utilizzare la modalità 1 anche per un utilizzo già deprecato.

Quando preferire la modalità 3 rispetto alla modalità 1

La modalità 1 è perfettamente utilizzabile in molti casi e potrebbe essere preferita alla modalità 3 nei casi in cui assumere la proprietà assumerebbe altrimenti la forma di spostare il puntatore intelligente su una variabile locale come reversednell'esempio sopra. Tuttavia, posso vedere due motivi per preferire la modalità 3 nel caso più generale:

  • È leggermente più efficiente passare un riferimento che creare un temporaneo e annullare il vecchio puntatore (la gestione del denaro è alquanto laboriosa); in alcuni scenari il puntatore può essere passato più volte invariato a un'altra funzione prima di essere effettivamente rubato. Tale passaggio richiederà generalmente la scrittura std::move(a meno che non venga utilizzata la modalità 2), ma si noti che questo è solo un cast che in realtà non fa nulla (in particolare senza dereferenziazione), quindi ha costi zero associati.

  • Dovrebbe essere ipotizzabile che qualcosa generi un'eccezione tra l'inizio della chiamata di funzione e il punto in cui (o qualche chiamata contenuta) sposta effettivamente l'oggetto puntato in un'altra struttura di dati (e questa eccezione non è già rilevata all'interno della funzione stessa ), quindi quando si utilizza la modalità 1, l'oggetto a cui fa riferimento il puntatore intelligente verrà distrutto prima che una catchclausola possa gestire l'eccezione (perché il parametro della funzione è stato distrutto durante lo svolgimento dello stack), ma non così quando si utilizza la modalità 3. Quest'ultimo fornisce il chiamante ha la possibilità di recuperare i dati dell'oggetto in tali casi (catturando l'eccezione). Si noti che la modalità 1 qui non causa una perdita di memoria , ma può portare a una perdita irreversibile di dati per il programma, che potrebbe anche essere indesiderabile.

Restituzione di un puntatore intelligente: sempre in base al valore

Per concludere una parola sulla restituzione di un puntatore intelligente, presumibilmente indicando un oggetto creato per essere utilizzato dal chiamante. Questo non è davvero un caso analogo con il passare puntatori in funzioni, ma per completezza vorrei insistere sul fatto che in questi casi tornare sempre per valore (e non utilizzare std::move nella returndichiarazione). Nessuno vuole ottenere un riferimento a un puntatore che probabilmente è stato appena annullato.


1
+1 per la modalità 0 - passando il puntatore sottostante invece di unique_ptr. Un po 'fuori tema (poiché la domanda riguarda il passaggio di unique_ptr) ma è semplice ed evita problemi.
Machta,

"La modalità 1 qui non causa una perdita di memoria " - ciò implica che la modalità 3 causa una perdita di memoria, il che non è vero. Indipendentemente dal fatto che unique_ptrsia stato spostato o meno, eliminerà comunque il valore se lo mantiene comunque ogni volta che viene distrutto o riutilizzato.
Rustyx,

@RustyX: Non riesco a vedere come tu abbia interpretato quell'implicazione e non ho mai avuto intenzione di dire ciò che pensi di aver implicato. Tutto ciò che intendevo dire è che, come altrove, l'uso di unique_ptrimpedisce una perdita di memoria (e quindi in un certo senso soddisfa il suo contratto), ma qui (cioè, utilizzando la modalità 1) potrebbe causare (in circostanze specifiche) qualcosa che potrebbe essere considerato ancora più dannoso , vale a dire una perdita di dati (distruzione del valore indicato) che avrebbe potuto essere evitata utilizzando la modalità 3.
Marc van Leeuwen,

4

Sì, devi prenderlo unique_ptrper valore nel costruttore. La semplicità è una cosa carina. Dato che non unique_ptrè duplicabile (copiatrice privata), ciò che hai scritto dovrebbe darti un errore del compilatore.


3

Modifica: questa risposta è errata, anche se, a rigore, il codice funziona. Lo lascio qui solo perché la discussione sotto di essa è troppo utile. Quest'altra risposta è la migliore risposta data al momento dell'ultima modifica: come posso passare un argomento unique_ptr a un costruttore o a una funzione?

L'idea di base ::std::moveè che le persone che ti stanno passando unique_ptrdovrebbero usarlo per esprimere la consapevolezza di sapere unique_ptrche stanno per perdere la proprietà.

Ciò significa che dovresti utilizzare un riferimento al valore a unique_ptrnei tuoi metodi, non a unique_ptrse stesso. Questo non funzionerà comunque perché passare in un vecchio normale unique_ptrrichiederebbe una copia, ed è esplicitamente vietato nell'interfaccia per unique_ptr. È interessante notare che l'uso di un riferimento rvalue denominato lo trasforma nuovamente in un valore lvalue, quindi è necessario utilizzare anche ::std::move all'interno dei metodi.

Ciò significa che i tuoi due metodi dovrebbero apparire così:

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

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

Quindi le persone che usano i metodi farebbero questo:

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

Come vedi, l' ::std::moveespressione indica che il puntatore perderà la proprietà nel punto in cui è più pertinente e utile sapere. Se ciò accadesse in modo invisibile, sarebbe molto confuso per le persone che usano la tua classe objptrperdere improvvisamente la proprietà senza una ragione evidente.


2
I riferimenti ai valori nominali sono valori.
R. Martinho Fernandes,

sei sicuro che lo sia Base fred(::std::move(objptr));e no Base::UPtr fred(::std::move(objptr));?
codablank1,

1
Per aggiungere al mio commento precedente: questo codice non verrà compilato. È ancora necessario utilizzare std::movel'implementazione sia del costruttore che del metodo. E anche quando passi per valore, il chiamante deve comunque usare std::moveper passare i valori. La differenza principale è che con il valore pass-by quell'interfaccia rende evidente che la proprietà andrà persa. Vedi il commento di Nicol Bolas su un'altra risposta.
R. Martinho Fernandes,

@ codablank1: Sì. Sto dimostrando come utilizzare il costruttore e i metodi in base che accettano riferimenti a valori.
Onnipotente il

@ R.MartinhoFernandes: Oh, interessante. Suppongo che abbia un senso. Mi aspettavo che avessi torto, ma i test effettivi hanno dimostrato che hai ragione. Riparato ora.
Onnipotente il

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

dovrebbe essere molto meglio

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

e

void setNext(Base::UPtr n)

dovrebbe essere

void setNext(Base::UPtr&& n)

con lo stesso corpo.

E ... cosa c'è evtdentro handle()??


3
Non c'è alcun vantaggio nell'utilizzare std::forwardqui: Base::UPtr&&è sempre un tipo di riferimento rvalue e lo std::movepassa come rvalue. È già inoltrato correttamente.
R. Martinho Fernandes,

7
Sono fortemente in disaccordo. Se una funzione accetta un unique_ptrvalore, allora sei sicuro che un costruttore di mosse è stato chiamato sul nuovo valore (o semplicemente che ti è stato assegnato un temporaneo). Questo assicura che la unique_ptrvariabile che l'utente ha ora sia vuota . Se lo prendi &&invece, verrà svuotato solo se il tuo codice invoca un'operazione di spostamento. A modo tuo, è possibile per la variabile da cui l'utente non deve essere stato spostato. Ciò rende l'utente std::movesospetto e confuso. L'uso std::movedovrebbe sempre garantire che qualcosa sia stato spostato .
Nicol Bolas,

@NicolBolas: hai ragione. Eliminerò la mia risposta perché mentre funziona, la tua osservazione è assolutamente corretta.
Onnipotente il

0

Alla risposta più votata. Preferisco passare per riferimento al valore.

Capisco qual è il problema che può derivare dal passaggio in base al riferimento al valore. Dividiamo questo problema su due lati:

  • per chiamante:

Devo scrivere il codice Base newBase(std::move(<lvalue>))o Base newBase(<rvalue>).

  • per la chiamata:

L'autore della libreria dovrebbe garantire che sposterà effettivamente unique_ptr per inizializzare il membro se desidera possedere la proprietà.

È tutto.

Se passi per riferimento al valore, invocherà solo un'istruzione "move", ma se passa per valore, è due.

Sì, se l'autore della biblioteca non è esperto in questo, potrebbe non spostare unique_ptr per inizializzare il membro, ma è il problema dell'autore, non tu. Qualunque cosa passi per valore o riferimento al valore, il tuo codice è lo stesso!

Se stai scrivendo una libreria, ora sai che dovresti garantirla, quindi fallo e basta, passare per riferimento al valore è una scelta migliore del valore. Il client che usa la tua libreria scriverà lo stesso codice.

Ora, per la tua domanda. Come faccio a passare un argomento unique_ptr a un costruttore o a una funzione?

Sai qual è la scelta migliore.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.