Copia il costruttore per una classe con unique_ptr


105

Come si implementa un costruttore di copia per una classe che ha una unique_ptrvariabile membro? Sto solo considerando C ++ 11.


9
Bene, cosa vuoi che faccia il costruttore di copie?
Nicol Bolas

Ho letto che unique_ptr non è copiabile. Questo mi fa pensare a come utilizzare una classe che ha una variabile membro unique_ptr in un file std::vector.
codefx

2
@AbhijitKadam Puoi fare una copia completa del contenuto di unique_ptr. In effetti, questa è spesso la cosa sensata da fare.
cubo

2
Tieni presente che probabilmente stai facendo la domanda sbagliata. Probabilmente non vuoi un costruttore di copia per la tua classe contenente un unique_ptr, probabilmente vuoi un costruttore di mosse, se il tuo obiettivo è mettere i dati in un file std::vector. D'altra parte, lo standard C ++ 11 ha creato automaticamente costruttori di spostamento, quindi forse vuoi un costruttore di copia ...
Yakk - Adam Nevraumont

3
Gli elementi vettoriali @codefx non devono essere copiabili; significa solo che il vettore non potrà essere copiato.
MM

Risposte:


81

Poiché unique_ptrnon può essere condiviso, è necessario copiarne il contenuto o convertirlo unique_ptrin un file shared_ptr.

class A
{
   std::unique_ptr< int > up_;

public:
   A( int i ) : up_( new int( i ) ) {}
   A( const A& a ) : up_( new int( *a.up_ ) ) {}
};

int main()
{
   A a( 42 );
   A b = a;
}

Puoi, come accennato NPE, utilizzare un move-ctor invece di un copy-ctor ma ciò comporterebbe una semantica diversa della tua classe. Un traslocatore dovrebbe rendere il membro mobile esplicitamente tramite std::move:

A( A&& a ) : up_( std::move( a.up_ ) ) {}

Avere un set completo degli operatori necessari porta anche a

A& operator=( const A& a )
{
   up_.reset( new int( *a.up_ ) );
   return *this,
}

A& operator=( A&& a )
{
   up_ = std::move( a.up_ );
   return *this,
}

Se vuoi usare la tua classe in a std::vector, devi fondamentalmente decidere se il vettore deve essere l'unico proprietario di un oggetto, nel qual caso sarebbe sufficiente rendere la classe mobile, ma non copiabile. Se tralasci il copy-ctor e il copy-assignment, il compilatore ti guiderà su come usare uno std :: vector con i tipi di solo spostamento.


4
Potrebbe valere la pena menzionare i costruttori di mosse?
NPE

4
+1, ma il costruttore di mosse dovrebbe essere enfatizzato ancora di più. In un commento, l'OP afferma che l'obiettivo è utilizzare l'oggetto in un vettore. Per questo, spostare la costruzione e spostare l'assegnazione sono le uniche cose richieste.
jogojapan

36
Come avvertimento, la strategia di cui sopra funziona per tipi semplici come int. Se si dispone di un file unique_ptr<Base>che memorizza a Derived, quanto sopra verrà tagliato.
Yakk - Adam Nevraumont

5
Non c'è controllo per null, così com'è questo consente una dereferenziazione nullptr. Che ne diciA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining

1
@Aaron in situazioni polimorfiche, il deleter verrà cancellato in qualche modo o inutile (se conosci il tipo da eliminare, perché cambiare solo il deleter?). In ogni caso, sì, questo è il design di un value_ptr- unique_ptrpiù informazioni su deleter / fotocopiatrice.
Yakk - Adam Nevraumont

47

Il caso usuale per avere un unique_ptrin una classe è quello di essere in grado di usare l'ereditarietà (altrimenti anche un oggetto semplice spesso andrebbe bene, vedi RAII). Per questo caso, finora non esiste una risposta appropriata in questo thread .

Quindi, ecco il punto di partenza:

struct Base
{
    //some stuff
};

struct Derived : public Base
{
    //some stuff
};

struct Foo
{
    std::unique_ptr<Base> ptr;  //points to Derived or some other derived class
};

... e l'obiettivo è, come detto, quello di renderlo Foocopiabile.

Per questo, è necessario eseguire una copia completa del puntatore contenuto per assicurarsi che la classe derivata venga copiata correttamente.

Ciò può essere ottenuto aggiungendo il codice seguente:

struct Base
{
    //some stuff

    auto clone() const { return std::unique_ptr<Base>(clone_impl()); }
protected:
    virtual Base* clone_impl() const = 0;
};

struct Derived : public Base
{
    //some stuff

protected:
    virtual Derived* clone_impl() const override { return new Derived(*this); };                                                 
};

struct Foo
{
    std::unique_ptr<Base> ptr;  //points to Derived or some other derived class

    //rule of five
    ~Foo() = default;
    Foo(Foo const& other) : ptr(other.ptr->clone()) {}
    Foo(Foo && other) = default;
    Foo& operator=(Foo const& other) { ptr = other.ptr->clone(); return *this; }
    Foo& operator=(Foo && other) = default;
};

Ci sono fondamentalmente due cose che stanno succedendo qui:

  • Il primo è l'aggiunta dei costruttori di copia e spostamento, che vengono implicitamente eliminati Fooquando unique_ptrviene eliminato il costruttore di copia di . Il costruttore di spostamenti può essere aggiunto semplicemente da = default... che è solo per far sapere al compilatore che il solito costruttore di spostamenti non deve essere cancellato (funziona, poiché unique_ptrha già un costruttore di spostamenti che può essere utilizzato in questo caso).

    Per il costruttore di copia di Foo, non esiste un meccanismo simile in quanto non esiste un costruttore di copia di unique_ptr. Quindi, bisogna costruirne una nuova unique_ptr, riempirla con una copia della punta originale e usarla come membro della classe copiata.

  • In caso di eredità, la copia delle punte originali deve essere eseguita con cura. Il motivo è che fare una semplice copia tramite std::unique_ptr<Base>(*ptr)nel codice sopra risulterebbe in slicing, cioè, solo il componente di base dell'oggetto viene copiato, mentre la parte derivata è mancante.

    Per evitare ciò, la copia deve essere eseguita tramite il modello di clone. L'idea è di fare la copia tramite una funzione virtuale clone_impl()che restituisce a Base*nella classe base. Nella classe derivata, tuttavia, viene esteso tramite covarianza per restituire a Derived*e questo puntatore punta a una copia appena creata della classe derivata. La classe base può quindi accedere a questo nuovo oggetto tramite il puntatore della classe base Base*, racchiuderlo in un unique_ptre restituirlo tramite la clone()funzione effettiva che viene chiamata dall'esterno.


3
Questa avrebbe dovuto essere la risposta accettata. Tutti gli altri stanno girando in tondo in questo thread, senza accennare al motivo per cui si vorrebbe copiare un oggetto puntato da unique_ptrquando il contenimento diretto farebbe altrimenti. La risposta??? Eredità .
Tanveer Badar

4
Si può usare unique_ptr anche quando si conosce il tipo concreto a cui si punta per una serie di motivi: 1. Deve essere annullabile. 2. La punta è molto grande e potremmo avere uno spazio di pila limitato. Spesso (1) e (2) andranno insieme, quindi una forza in occasione preferisce unique_ptrover optionalper i tipi nullable.
Ponkadoodle

3
L'idioma del brufolo è un altro motivo.
emsr

E se una classe base non fosse astratta? Lasciarlo senza pure-specifier può portare a bug in fase di esecuzione se si dimentica di reimplementarlo in derivato.
olek stolar

1
@ OleksijPlotnyc'kyj: sì, se implementi clone_implin base, il compilatore non ti dirà se lo dimentichi nella classe derivata. Tuttavia, potresti usare un'altra classe base Cloneablee implementare un virtuale puro clone_impllì. Quindi il compilatore si lamenterà se lo dimentichi nella classe derivata.
davidhigh

11

Prova questo helper per creare copie profonde e gestisci quando l'origine unique_ptr è null.

    template< class T >
    std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
    {
        return source ? std::make_unique<T>(*source) : nullptr;
    }

Per esempio:

class My
{
    My( const My& rhs )
        : member( copy_unique(rhs.member) )
    {
    }

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};

2
Copierà correttamente se la sorgente punta a qualcosa derivato da T?
Roman Shapovalov

3
@RomanShapovalov No, probabilmente no, potresti affettare. In tal caso, la soluzione sarebbe probabilmente quella di aggiungere un metodo virtual unique_ptr <T> clone () al tipo T e fornire sostituzioni del metodo clone () nei tipi derivati ​​da T. Il metodo clone creerebbe una nuova istanza di il tipo derivato e restituirlo.
Scott Langham

Non ci sono puntatori univoci / con ambito in c ++ o librerie boost che hanno la funzionalità di copia profonda incorporata? Sarebbe bello non dover creare i nostri costruttori di copia personalizzati, ecc. Per le classi che utilizzano questi puntatori intelligenti, quando vogliamo il comportamento della copia profonda, come spesso accade. Mi chiedevo solo.
shadow_map

5

Daniel Frey parla della soluzione di copia, vorrei parlare di come spostare unique_ptr

#include <memory>
class A
{
  public:
    A() : a_(new int(33)) {}

    A(A &&data) : a_(std::move(data.a_))
    {
    }

    A& operator=(A &&data)
    {
      a_ = std::move(data.a_);
      return *this;
    }

  private:
    std::unique_ptr<int> a_;
};

Si chiamano costruttore di mosse e assegnazione di mosse

potresti usarli in questo modo

int main()
{
  A a;
  A b(std::move(a)); //this will call move constructor, transfer the resource of a to b

  A c;
  a = std::move(c); //this will call move assignment, transfer the resource of c to a

}

Devi racchiudere aec con std :: move perché hanno un nome std :: move sta dicendo al compilatore di trasformare il valore in un riferimento rvalue qualunque siano i parametri In senso tecnico, std :: move è un'analogia con qualcosa di simile " std :: rvalue"

Dopo lo spostamento, la risorsa di unique_ptr viene trasferita a un altro unique_ptr

Ci sono molti argomenti che documentano il riferimento rvalue; questo è abbastanza facile per cominciare .

Modificare :

Lo stato dell'oggetto spostato rimarrà valido ma non specificato .

Il primer C ++ 5, ch13 fornisce anche un'ottima spiegazione su come "spostare" l'oggetto


1
quindi cosa succede all'oggetto adopo aver chiamato std :: move (a) nel bcostruttore di spostamento? È solo totalmente non valido?
David Doria

3

Suggerisco di usare make_unique

class A
{
   std::unique_ptr< int > up_;

public:
   A( int i ) : up_(std::make_unique<int>(i)) {}
   A( const A& a ) : up_(std::make_unique<int>(*a.up_)) {};

int main()
{
   A a( 42 );
   A b = a;
}

-1

unique_ptr non è copiabile, è solo mobile.

Ciò influenzerà direttamente Test, che è, nel secondo esempio, anche solo mobile e non copiabile.

In effetti, è bene che tu usi ciò unique_ptrche ti protegge da un grosso errore.

Ad esempio, il problema principale con il tuo primo codice è che il puntatore non viene mai eliminato, il che è davvero, davvero brutto. Di ', potresti risolvere questo problema:

class Test
{
    int* ptr; // writing this in one line is meh, not sure if even standard C++

    Test() : ptr(new int(10)) {}
    ~Test() {delete ptr;}
};

int main()
{       
     Test o;
     Test t = o;
}

Anche questo è un male. Cosa succede se copi Test? Ci saranno due classi che hanno un puntatore che punta allo stesso indirizzo.

Quando uno Testviene distrutto, distruggerà anche il puntatore. Quando il tuo secondo Testviene distrutto, cercherà di rimuovere anche la memoria dietro il puntatore. Ma è già stato cancellato e avremo qualche errore di runtime di accesso alla memoria non valido (o comportamento indefinito se siamo sfortunati).

Quindi, il modo giusto è implementare il costruttore di copia e l'operatore di assegnazione della copia, in modo che il comportamento sia chiaro e possiamo creare una copia.

unique_ptrè molto più avanti di noi qui. Ha il significato semantico: " Io sono unique, quindi non puoi semplicemente copiarmi " . Quindi, ci impedisce l'errore di implementare ora gli operatori a portata di mano.

È possibile definire il costruttore di copia e l'operatore di assegnazione della copia per un comportamento speciale e il codice funzionerà. Ma tu sei, giustamente (!), Costretto a farlo.

La morale della favola: usare sempre unique_ptrin questo tipo di situazioni.

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.