Konstruktor kopiujący dla klasy z unique_ptr

105

Jak zaimplementować konstruktora kopiującego dla klasy, która ma unique_ptrzmienną składową? Rozważam tylko C ++ 11.

codefx
źródło
9
Co chcesz, aby konstruktor kopiujący zrobił?
Nicol Bolas
Czytałem, że unique_ptr jest nie do skopiowania. To sprawia, że ​​zastanawiam się, jak użyć klasy, która ma zmienną składową unique_ptr w pliku std::vector.
codefx Kwietnia
2
@AbhijitKadam Możesz zrobić dokładną kopię zawartości unique_ptr. W rzeczywistości jest to często rozsądne rozwiązanie.
Sześcienny
2
Pamiętaj, że prawdopodobnie zadajesz niewłaściwe pytanie. Prawdopodobnie nie chcesz konstruktora kopiującego dla swojej klasy zawierającej a unique_ptr, prawdopodobnie potrzebujesz konstruktora przenoszenia, jeśli Twoim celem jest umieszczenie danych w pliku std::vector. Z drugiej strony standard C ++ 11 automatycznie utworzył konstruktory przenoszenia, więc może potrzebujesz konstruktora kopiującego ...
Yakk - Adam Nevraumont
3
Elementy wektorowe @codefx nie muszą być kopiowalne; oznacza to po prostu, że wektor nie będzie kopiowalny.
MM

Odpowiedzi:

81

Ponieważ unique_ptrnie można udostępniać, musisz albo głęboko skopiować jego zawartość, albo przekonwertować unique_ptrplik 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;
}

Możesz, jak wspomniano w NPE, użyć narzędzia do przenoszenia zamiast edytora kopii, ale spowodowałoby to inną semantykę twojej klasy. Operator ruchu musiałby uczynić członka jako ruchomym jawnie poprzez std::move:

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

Posiadanie pełnego zestawu niezbędnych operatorów również prowadzi do

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

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

Jeśli chcesz użyć swojej klasy w a std::vector, po prostu musisz zdecydować, czy wektor będzie unikalnym właścicielem obiektu, w takim przypadku wystarczy uczynić klasę ruchomą, ale nie kopiowalną. Jeśli pominiesz program copy-ctor i copy-assignment, kompilator pokieruje Cię, jak używać std :: vector z typami tylko do przenoszenia.

Daniel Frey
źródło
4
Może warto wspomnieć o konstruktorach przenoszenia?
NPE
4
+1, ale konstruktor ruchu należy jeszcze bardziej podkreślić. W komentarzu OP mówi, że celem jest użycie obiektu w wektorze. W tym celu wystarczy przenieść konstrukcję i przypisanie ruchu.
jogojapan
36
Jako ostrzeżenie, powyższa strategia działa dla prostych typów, takich jak int. Jeśli masz, unique_ptr<Base>który przechowuje a Derived, powyższe będzie cięło.
Yakk - Adam Nevraumont
5
Nie ma sprawdzania null, więc tak jak jest, pozwala to na dereferencję nullptr. Co powiesz naA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining
1
@Aaron w sytuacjach polimorficznych deleter zostanie w jakiś sposób skasowany lub bezcelowy (jeśli znasz typ do usunięcia, po co zmieniać tylko deleter?). W każdym razie tak, jest to projekt informacji o wartości plus value_ptr- unique_ptrplus usuwającej / kopiującej.
Yakk - Adam Nevraumont
47

Typowym przypadkiem, w którym ktoś ma a unique_ptrw klasie, jest możliwość użycia dziedziczenia (w przeciwnym razie zwykły obiekt również często by działał, patrz RAII). W tym przypadku do tej pory nie ma odpowiedniej odpowiedzi w tym wątku .

Oto punkt wyjścia:

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

... a celem jest, jak powiedziano, uczynienie go Foomożliwym do skopiowania.

W tym celu należy wykonać głęboką kopię zawartego wskaźnika, aby zapewnić poprawne skopiowanie klasy pochodnej.

Można to osiągnąć, dodając następujący kod:

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

Zasadniczo mają tu miejsce dwie rzeczy:

  • Pierwszym z nich jest dodanie konstruktorów kopiowania i przenoszenia, które są niejawnie usuwane, Foogdy konstruktor kopiujący unique_ptrjest usuwany. Konstruktor przenoszenia można dodać po prostu przez = default... co ma na celu tylko poinformowanie kompilatora, że ​​zwykły konstruktor przenoszenia nie powinien zostać usunięty (działa to, ponieważ unique_ptrma już konstruktor przenoszenia, którego można użyć w tym przypadku).

    W przypadku konstruktora kopiującego programu Foonie ma podobnego mechanizmu, ponieważ nie ma konstruktora kopiującego programu unique_ptr. Trzeba więc skonstruować nowy unique_ptr, wypełnić go kopią oryginalnego pointee i użyć go jako członka skopiowanej klasy.

  • W przypadku dziedziczenia kopia oryginalnego wskazanego musi być wykonana starannie. Powodem jest to, że wykonanie prostej kopii przez std::unique_ptr<Base>(*ptr)w powyższym kodzie skutkowałoby cięciem na plasterki, tj. Kopiowany byłby tylko podstawowy komponent obiektu, podczas gdy brakuje części pochodnej.

    Aby tego uniknąć, kopię należy wykonać za pomocą wzorca klonowania. Chodzi o to, aby wykonać kopię za pomocą funkcji wirtualnej, clone_impl()która zwraca Base*w klasie bazowej. Jednak w klasie pochodnej jest on rozszerzany za pomocą kowariancji w celu zwrócenia a Derived*, a ten wskaźnik wskazuje na nowo utworzoną kopię klasy pochodnej. Klasa bazowa może następnie uzyskać dostęp do tego nowego obiektu za pośrednictwem wskaźnika klasy bazowej Base*, zawinąć go w a unique_ptri zwrócić za pośrednictwem rzeczywistej clone()funkcji wywoływanej z zewnątrz.

davidhigh
źródło
3
To powinna być akceptowana odpowiedź. Wszyscy inni krążą w tym wątku w kółko, nie wspominając, dlaczego ktoś miałby chcieć skopiować obiekt wskazany przez, unique_ptrpodczas gdy bezpośrednie powstrzymanie zrobiłoby inaczej. Odpowiedź??? Dziedziczenie .
Tanveer Badar
4
Można używać unique_ptr, nawet jeśli znają konkretny typ wskazywany z różnych powodów: 1. Musi mieć wartość null. 2. Pointee jest bardzo duży i możemy mieć ograniczoną przestrzeń w stosie. Często (1) i (2) pójdą razem, stąd można by przy okazji wolą unique_ptrponad optionaldla pustych typów.
Ponkadoodle
3
Idiom pryszcz to kolejny powód.
emsr
A co, jeśli klasa bazowa nie powinna być abstrakcyjna? Pozostawienie go bez czystego specyfikatora może prowadzić do błędów w czasie wykonywania, jeśli zapomnisz zaimplementować go ponownie w środowisku pochodnym.
olek stolar
1
@ OleksijPlotnyc'kyj: tak, jeśli zaimplementujesz clone_implin base, kompilator nie powie Ci, jeśli zapomnisz o tym w klasie pochodnej. Możesz jednak użyć innej klasy bazowej Cloneablei zaimplementować tam czystą wirtualną clone_impl. Wtedy kompilator będzie narzekał, jeśli zapomnisz o tym w klasie pochodnej.
davidhigh
11

Wypróbuj tego pomocnika, aby tworzyć głębokie kopie i radzić sobie, gdy źródło unique_ptr ma wartość null.

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

Na przykład:

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

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};
Scott Langham
źródło
2
Czy poprawnie skopiuje, jeśli źródło wskazuje na coś pochodzącego z T?
Roman Shapovalov
3
@RomanShapovalov Nie, prawdopodobnie nie, dostaniesz krojenie. W takim przypadku rozwiązaniem prawdopodobnie byłoby dodanie wirtualnej metody unique_ptr <T> clone () do typu T i zapewnienie nadpisań metody clone () w typach pochodzących z T. Metoda clone utworzyłaby nowe wystąpienie metody clone () typ pochodny i zwróć to.
Scott Langham,
Czy nie ma wskaźników unikatowych / o określonym zakresie w bibliotekach C ++ lub boost, które mają wbudowaną funkcję głębokiego kopiowania? Byłoby miło, gdybyśmy nie musieli tworzyć naszych własnych konstruktorów kopiujących itp. Dla klas, które używają tych inteligentnych wskaźników, gdy chcemy zachować głębokie kopiowanie, co często ma miejsce. Po prostu się zastanawiam.
shadow_map
5

Daniel Frey wspomniał o rozwiązaniu do kopiowania, chciałbym porozmawiać o tym, jak przenieść 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_;
};

Nazywa się je konstruktorem przenoszenia i przypisaniem przenoszenia

możesz ich użyć w ten sposób

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

}

Musisz zawinąć a i c std :: move, ponieważ mają one nazwę std :: move mówi kompilatorowi, aby przekształcił wartość na odniesienie rvalue niezależnie od parametrów. W technicznym sensie std :: move jest analogią do czegoś takiego jak " std :: rvalue "

Po przeniesieniu zasób z unique_ptr jest przenoszony do innego unique_ptr

Istnieje wiele tematów, które dokumentują odniesienie do wartości r; jest to dość łatwe na początek .

Edytować :

Przeniesiony obiekt pozostanie ważny, ale stan nieokreślony .

C ++ primer 5, ch13 również daje bardzo dobre wyjaśnienie, jak „przesuwać” obiekt

StereoMatching
źródło
1
więc co się dzieje z obiektem apo wywołaniu std :: move (a) w bkonstruktorze przenoszenia? Czy to jest po prostu całkowicie nieważne?
David Doria
3

Proponuję użyć 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;
}
Pluśnięcie
źródło
-1

unique_ptr nie można go kopiować, można go tylko przenosić.

Wpłynie to bezpośrednio na Test, który w twoim drugim przykładzie jest również możliwy do przeniesienia i nie do skopiowania.

W rzeczywistości dobrze jest, gdy używasz, unique_ptrktóry chroni cię przed dużym błędem.

Na przykład głównym problemem związanym z pierwszym kodem jest to, że wskaźnik nigdy nie jest usuwany, co jest naprawdę, bardzo złe. Powiedzmy, że możesz to naprawić przez:

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

To też jest złe. Co się stanie, jeśli skopiujesz Test? Będą dwie klasy, które będą miały wskaźnik wskazujący na ten sam adres.

Kiedy jeden Testzostanie zniszczony, zniszczy również wskaźnik. Kiedy twoja sekunda Testzostanie zniszczona, spróbuje również usunąć pamięć za wskaźnikiem. Ale został już usunięty i otrzymamy zły błąd w czasie wykonywania dostępu do pamięci (lub niezdefiniowane zachowanie, jeśli mamy pecha).

Zatem właściwym sposobem jest albo zaimplementowanie konstruktora kopiującego, jak i operatora przypisania kopiującego, aby zachowanie było jasne i można było utworzyć kopię.

unique_ptrwyprzedza nas tutaj. Ma znaczenie semantyczne: „ Jestem unique, więc nie możesz mnie tak po prostu skopiować. ” Tak więc, zapobiega to błędowi implementacji dostępnych operatorów.

Możesz zdefiniować konstruktora kopiującego i operatora przypisania kopii dla specjalnego zachowania, a kod będzie działał. Ale słusznie (!) Jesteś do tego zmuszony.

Morał tej historii: zawsze używaj unique_ptrw tego rodzaju sytuacjach.

Lód Ogień
źródło