Restituisce unique_ptr dalle funzioni


367

unique_ptr<T>non consente la costruzione di copie, ma supporta la semantica di spostamento. Tuttavia, posso restituire a unique_ptr<T>da una funzione e assegnare il valore restituito a una variabile.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

Il codice sopra si compila e funziona come previsto. Quindi, come mai quella linea 1non invoca il costruttore di copie e provoca errori del compilatore? Se invece dovessi usare line 2avrebbe senso (usare anche line 2funziona, ma non è necessario farlo).

So che C ++ 0x consente questa eccezione unique_ptrpoiché il valore restituito è un oggetto temporaneo che verrà distrutto non appena la funzione uscirà, garantendo così l'unicità del puntatore restituito. Sono curioso di sapere come questo viene implementato, è inserito nel compilatore o c'è qualche altra clausola nella specifica del linguaggio che questo sfrutta?


Ipoteticamente, se implementassi un metodo factory , preferiresti 1 o 2 per restituire l'output della factory? Presumo che questo sarebbe l'uso più comune di 1 perché, con una fabbrica adeguata, in realtà si desidera che la proprietà dell'oggetto costruito passi al chiamante.
Xharlie,

7
@Xharlie? Entrambi passano la proprietà del unique_ptr. L'intera domanda è che 1 e 2 sono due modi diversi per ottenere la stessa cosa.
Pretoriano,

in questo caso, l'RVO si svolge anche in c ++ 0x, la distruzione dell'oggetto unique_ptr verrà eseguita una volta eseguita dopo la chiusura della mainfunzione, ma non alla foochiusura.
Ampawd

Risposte:


219

c'è qualche altra clausola nella specifica del linguaggio che questo sfrutta?

Sì, vedere 12.8 §34 e §35:

Quando vengono soddisfatti determinati criteri, un'implementazione è autorizzata a omettere la costruzione di copia / spostamento di un oggetto di classe [...] Questa elisione delle operazioni di copia / spostamento, chiamata elisione di copia , è consentita [...] in una dichiarazione di ritorno in una funzione con un tipo restituito di classe, quando l'espressione è il nome di un oggetto automatico non volatile con lo stesso tipo non qualificato cv del tipo restituito di funzione [...]

Quando i criteri per l'elissione di un'operazione di copia sono soddisfatti e l'oggetto da copiare è designato da un valore, la risoluzione di sovraccarico per selezionare il costruttore per la copia viene prima eseguita come se l'oggetto fosse designato da un valore .


Volevo solo aggiungere un altro punto secondo cui il ritorno in base al valore dovrebbe essere la scelta predefinita qui perché un valore denominato nell'istruzione return nel caso peggiore, ovvero senza elisioni in C ++ 11, C ++ 14 e C ++ 17 viene trattato come valore. Quindi, ad esempio, la seguente funzione viene compilata con il -fno-elide-constructorsflag

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

Con il flag impostato sulla compilation ci sono due mosse (1 e 2) in corso in questa funzione e poi una mossa più avanti (3).


@juanchopanza Intendi essenzialmente che foo()sta per essere distrutto (se non fosse assegnato a nulla), proprio come il valore restituito all'interno della funzione, e quindi ha senso che C ++ usi un costruttore di mosse quando lo fa unique_ptr<int> p = foo();?
7

1
Questa risposta dice che un'implementazione è autorizzata a fare qualcosa ... non dice che deve, quindi se questa fosse l'unica sezione rilevante, ciò implicherebbe basarsi su questo comportamento non è portatile. Ma non credo sia giusto. Sono propenso a pensare che la risposta corretta abbia più a che fare con il costruttore di mosse, come descritto nella risposta di Nikola Smiljanic e Bartosz Milewski.
Don Hatch,

6
@DonHatch Dice che in questi casi è "consentito" eseguire le elisioni di copia / spostamento, ma qui non stiamo parlando di elisioni di copia. È il secondo paragrafo citato che si applica qui, che trasporta sulle spalle le regole di elisione della copia, ma non è la stessa elisione della copia. Non c'è incertezza nel secondo paragrafo: è totalmente portatile.
Joseph Mansfield,

@juanchopanza Mi rendo conto che adesso sono passati 2 anni, ma senti ancora che è sbagliato? Come ho già detto nel commento precedente, non si tratta di copia elisione. Accade così che nei casi in cui potrebbe essere applicata la copia elisione (anche se non può essere applicata con std::unique_ptr), esiste una regola speciale per trattare prima gli oggetti come valori. Penso che sia completamente d'accordo con ciò che Nikola ha risposto.
Joseph Mansfield,

1
Quindi perché visualizzo ancora l'errore "tentativo di fare riferimento a una funzione eliminata" per il mio tipo di solo spostamento (costruttore di copia rimosso) quando lo restituisco esattamente allo stesso modo di questo esempio?
DrumM,

104

Questo non è in alcun modo specifico std::unique_ptr, ma si applica a qualsiasi classe che sia mobile. È garantito dalle regole della lingua poiché si ritorna per valore. Il compilatore tenta di eludere le copie, invoca un costruttore di mosse se non può rimuovere copie, chiama un costruttore di copie se non può spostarsi e non riesce a compilare se non può copiare.

Se avessi una funzione che accetta std::unique_ptrcome argomento non saresti in grado di passarci sopra. Dovresti invocare esplicitamente il costruttore di spostamento, ma in questo caso non dovresti usare la variabile p dopo la chiamata a bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}

3
@Fred - beh, non proprio. Sebbene pnon sia temporaneo, il risultato di foo()ciò che viene restituito è; quindi è un valore e può essere spostato, il che rende possibile l'assegnazione main. Direi che avevi torto, tranne che Nikola sembra applicare questa regola a pse stesso, che è in errore.
Edward Strange,

Esattamente quello che volevo dire, ma non sono riuscito a trovare le parole. Ho rimosso quella parte della risposta poiché non era molto chiaro.
Nikola Smiljanić,

Ho una domanda: nella domanda originale, c'è qualche differenza sostanziale tra Linea 1e Linea 2? A mio avviso è la stessa da quando la costruzione pin main, esso soltanto si preoccupa per il tipo di tipo di ritorno di foo, giusto?
Hongxu Chen,

1
@HongxuChen In questo esempio non c'è assolutamente alcuna differenza, vedere la citazione dallo standard nella risposta accettata.
Nikola Smiljanić,

In realtà, puoi usare p in seguito, purché tu lo assegni. Fino ad allora, non puoi provare a fare riferimento ai contenuti.
Alan,

38

unique_ptr non ha il tradizionale costruttore di copie. Invece ha un "costruttore di spostamento" che utilizza riferimenti a valori:

unique_ptr::unique_ptr(unique_ptr && src);

Un riferimento di valore (la doppia e commerciale) si legherà solo a un valore. Ecco perché ricevi un errore quando provi a passare un valore_valore unique_ptr a una funzione. D'altra parte, un valore che viene restituito da una funzione viene trattato come un valore, quindi il costruttore di movimenti viene chiamato automaticamente.

A proposito, questo funzionerà correttamente:

bar(unique_ptr<int>(new int(44));

Univoco_ptr temporaneo qui è un valore.


8
Penso che il punto sia di più, perché p- "ovviamente" un valore - può essere trattato come un valore nella dichiarazione di ritorno return p;nella definizione di foo. Non credo che ci sia alcun problema con il fatto che il valore di ritorno della funzione stessa può essere "spostato".
CB Bailey,

Il wrapping del valore restituito dalla funzione in std :: move significa che verrà spostato due volte?

3
@RodrigoSalazar std :: move è solo un cast di fantasia da un riferimento lvalue (&) a un riferimento rvalue (&&). L'uso estraneo di std :: sposta su un valore di riferimento sarà semplicemente un noop
TiMoch

13

Penso che sia perfettamente spiegato nell'articolo 25 di Effective Modern C ++ di Scott Meyers . Ecco un estratto:

La parte della benedizione standard che la RVO continua dicendo che se le condizioni per la RVO sono soddisfatte, ma i compilatori scelgono di non eseguire l'elisione della copia, l'oggetto da restituire deve essere trattato come un valore. In effetti, lo Standard richiede che quando RVO è consentito, sia l'elezione della copia abbia luogo o std::movesia implicitamente applicata agli oggetti locali restituiti.

Qui, RVO si riferisce all'ottimizzazione del valore di ritorno , e se le condizioni per l'RVO sono soddisfatte significa restituire l'oggetto locale dichiarato all'interno della funzione che ci si aspetterebbe di fare l' RVO , che è anche ben spiegato al punto 25 del suo libro facendo riferimento a lo standard (qui l' oggetto locale include gli oggetti temporanei creati dall'istruzione return). Il più grande take away dall'estratto è o l'elezione della copia avviene o std::moveviene implicitamente applicata agli oggetti locali che vengono restituiti . Scott menziona l'articolo 25 che std::moveè implicitamente applicato quando il compilatore sceglie di non eludere la copia e il programmatore non dovrebbe esplicitamente farlo.

Nel tuo caso, il codice è chiaramente un candidato per RVO in quanto restituisce l'oggetto locale pe il tipo di pè lo stesso del tipo restituito, che si traduce in una copia elisione. E se il compilatore avesse scelto di non eludere la copia, per qualsiasi motivo, std::movesarebbe entrato in riga 1.


5

Una cosa che non ho visto in altre risposte èPer chiarire un'altra risposta che esiste una differenza tra il ritorno di std :: unique_ptr che è stato creato all'interno di una funzione, e quello che è stato dato a quella funzione.

L'esempio potrebbe essere così:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));

È menzionato nella risposta da fredoverflow - chiaramente evidenziato " oggetto automatico ". Un riferimento (incluso un riferimento al valore) non è un oggetto automatico.
Toby Speight,

@TobySpeight Ok, scusa. Immagino che il mio codice sia solo un chiarimento allora.
v010dya,
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.