Kopieren Sie den Konstruktor für eine Klasse mit unique_ptr

105

Wie implementiere ich einen Kopierkonstruktor für eine Klasse mit einer unique_ptrMitgliedsvariablen? Ich denke nur an C ++ 11.

codefx
quelle
9
Was soll der Kopierkonstruktor tun?
Nicol Bolas
Ich habe gelesen, dass unique_ptr nicht kopierbar ist. Ich frage mich daher, wie ich eine Klasse verwenden soll, die eine unique_ptr-Mitgliedsvariable in a hat std::vector.
Codefx
2
@AbhijitKadam Sie können eine tiefe Kopie des Inhalts von unique_ptr erstellen. In der Tat ist das oft die vernünftige Sache.
Cubic
2
Bitte beachten Sie, dass Sie möglicherweise die falsche Frage stellen. Sie möchten wahrscheinlich keinen Kopierkonstruktor für Ihre Klasse, der a enthält unique_ptr. Sie möchten wahrscheinlich einen Verschiebungskonstruktor, wenn Sie die Daten in a ablegen möchten std::vector. Auf der anderen Seite hat der C ++ 11-Standard automatisch Verschiebungskonstruktoren erstellt. Vielleicht möchten Sie also einen Kopierkonstruktor ...
Yakk - Adam Nevraumont
3
@codefx Vektorelemente müssen nicht kopierbar sein. es bedeutet nur, dass der Vektor nicht kopierbar ist.
MM

Antworten:

81

Da das unique_ptrnicht freigegeben werden kann, müssen Sie entweder seinen Inhalt tief kopieren oder das unique_ptrin ein konvertieren 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;
}

Sie können, wie NPE erwähnt, einen Verschiebungs-Ctor anstelle eines Kopier-Ctors verwenden, dies würde jedoch zu einer anderen Semantik Ihrer Klasse führen. Ein Move-Ctor müsste das Mitglied explizit beweglich machen über std::move:

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

Ein vollständiger Satz der erforderlichen Operatoren führt auch dazu

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

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

Wenn Sie Ihre Klasse in a verwenden möchten std::vector, müssen Sie grundsätzlich entscheiden, ob der Vektor der eindeutige Eigentümer eines Objekts sein soll. In diesem Fall würde es ausreichen, die Klasse beweglich, aber nicht kopierbar zu machen. Wenn Sie den Kopierer und die Kopierzuweisung weglassen, führt Sie der Compiler durch die Verwendung eines std :: -Vektors mit Nur-Verschieben-Typen.

Daniel Frey
quelle
4
Könnte es wert sein, Bewegungskonstruktoren zu erwähnen?
NPE
4
+1, aber der Verschiebungskonstruktor sollte noch stärker betont werden. In einem Kommentar sagt das OP, dass das Ziel darin besteht, das Objekt in einem Vektor zu verwenden. Dafür sind nur die Bewegungskonstruktion und die Bewegungszuweisung erforderlich.
Jogojapan
36
Als Warnung funktioniert die obige Strategie für einfache Typen wie int. Wenn Sie eine haben, die eine unique_ptr<Base>speichert Derived, wird die oben genannte in Scheiben geschnitten.
Yakk - Adam Nevraumont
4
Es gibt keine Überprüfung auf Null, so wie dies ist, ermöglicht dies eine Nullptr-Dereferenzierung. Wie wäre es mitA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining
1
@Aaron In polymorphen Situationen wird der Deleter vom Typ gelöscht oder irgendwie sinnlos (wenn Sie den zu löschenden Typ kennen, warum nur den Deleter ändern?). Auf jeden Fall, ja, das ist das Design ein value_ptr- unique_ptrund deleter / Kopierer Informationen.
Yakk - Adam Nevraumont
46

Der übliche Fall für unique_ptreine Klasse ist, dass sie eine Vererbung verwenden kann (andernfalls würde ein einfaches Objekt oft auch funktionieren, siehe RAII). Für diesen Fall gibt es in diesem Thread bisher keine passende Antwort .

Hier ist also der Ausgangspunkt:

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

... und das Ziel ist, wie gesagt, Fookopierbar zu machen .

Dazu muss eine tiefe Kopie des enthaltenen Zeigers erstellt werden, um sicherzustellen, dass die abgeleitete Klasse korrekt kopiert wird.

Dies kann durch Hinzufügen des folgenden Codes erreicht werden:

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

Grundsätzlich gibt es hier zwei Dinge:

  • Das erste ist das Hinzufügen von Kopier- und Verschiebungskonstruktoren, die implizit gelöscht werden, Foowenn der Kopierkonstruktor von unique_ptrgelöscht wird. Der Verschiebungskonstruktor kann einfach durch = default... hinzugefügt werden , um den Compiler darüber zu informieren, dass der übliche Verschiebungskonstruktor nicht gelöscht werden soll (dies funktioniert, da unique_ptrbereits ein Verschiebungskonstruktor vorhanden ist, der in diesem Fall verwendet werden kann).

    Für den Kopierkonstruktor von Foogibt es keinen ähnlichen Mechanismus wie für den Kopierkonstruktor von unique_ptr. Man muss also ein neues unique_ptrerstellen, es mit einer Kopie des ursprünglichen Pointees füllen und es als Mitglied der kopierten Klasse verwenden.

  • Falls es sich um eine Vererbung handelt, muss die Kopie des ursprünglichen Pointee sorgfältig erstellt werden. Der Grund dafür ist, dass ein einfaches Kopieren über std::unique_ptr<Base>(*ptr)im obigen Code zum Schneiden führen würde, dh nur die Basiskomponente des Objekts wird kopiert, während der abgeleitete Teil fehlt.

    Um dies zu vermeiden, muss das Kopieren über das Klonmuster erfolgen. Die Idee ist, die Kopie über eine virtuelle Funktion zu clone_impl()erstellen, die a Base*in der Basisklasse zurückgibt . In der abgeleiteten Klasse wird es jedoch über die Kovarianz erweitert, um a zurückzugeben Derived*, und dieser Zeiger zeigt auf eine neu erstellte Kopie der abgeleiteten Klasse. Die Basisklasse kann dann über den Basisklassenzeiger auf dieses neue Objekt zugreifen Base*, es in ein Objekt einschließen unique_ptrund es über die eigentliche clone()Funktion zurückgeben, die von außen aufgerufen wird.

Davidhigh
quelle
3
Dies sollte die akzeptierte Antwort gewesen sein. Alle anderen gehen in diesem Thread in Kreisen, ohne darauf hinzuweisen, warum man ein Objekt kopieren möchte, auf das verwiesen wird, unique_ptrwenn die direkte Eindämmung etwas anderes tun würde. Die Antwort??? Vererbung .
Tanveer Badar
4
Man kann unique_ptr verwenden, selbst wenn man weiß, auf welchen konkreten Typ aus verschiedenen Gründen verwiesen wird: 1. Er muss nullwertfähig sein. 2. Der Spitzenwert ist sehr groß und der Stapelspeicher ist möglicherweise begrenzt. Oft (1) und (2) werden zusammen gehen, damit man könnte gelegentlich lieber unique_ptrüber optionalfür Nullable Types.
Ponkadoodle
3
Die Pickelsprache ist ein weiterer Grund.
Emsr
Was ist, wenn eine Basisklasse nicht abstrakt sein sollte? Wenn Sie es ohne reinen Spezifizierer belassen, kann dies zu Laufzeitfehlern führen, wenn Sie vergessen, es in abgeleitet erneut zu implementieren.
Oleksij Plotnyc'kyj
1
@ OleksijPlotnyc'kyj: Ja, wenn Sie die clone_implIn-Base implementieren , sagt Ihnen der Compiler nicht, ob Sie sie in der abgeleiteten Klasse vergessen haben. Sie können jedoch eine andere Basisklasse verwenden Cloneableund dort eine reine virtuelle Klasse implementieren clone_impl. Dann beschwert sich der Compiler, wenn Sie es in der abgeleiteten Klasse vergessen.
Davidhigh
11

Versuchen Sie diesen Helfer, um tiefe Kopien zu erstellen und zu bewältigen, wenn die Quelle unique_ptr null ist.

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

Z.B:

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

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};
Scott Langham
quelle
2
Wird es korrekt kopiert, wenn die Quelle auf etwas verweist, das von T abgeleitet ist?
Roman Shapovalov
3
@ RomanShapovalov Nein, wahrscheinlich nicht, du würdest in Scheiben schneiden. In diesem Fall besteht die Lösung wahrscheinlich darin, Ihrem T-Typ eine virtuelle Methode unique_ptr <T> clone () hinzuzufügen und Überschreibungen der Methode clone () in von T abgeleiteten Typen bereitzustellen. Die Klonmethode würde eine neue Instanz von erstellen den abgeleiteten Typ und geben Sie das zurück.
Scott Langham
Gibt es in c ++ oder Boost-Bibliotheken keine eindeutigen Zeiger mit Gültigkeitsbereich, in die die Deep Copy-Funktionalität integriert ist? Es wäre schön, wenn wir unsere benutzerdefinierten Kopierkonstruktoren usw. nicht für Klassen erstellen müssten, die diese intelligenten Zeiger verwenden, wenn wir das Deep-Copy-Verhalten wünschen, was häufig der Fall ist. Ich wundere mich nur.
shadow_map
5

Daniel Frey erwähnt über Kopierlösung, ich würde darüber sprechen, wie man den unique_ptr verschiebt

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

Sie werden als Verschiebungskonstruktor und Verschiebungszuweisung bezeichnet

Sie könnten sie so verwenden

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

}

Sie müssen a und c mit std :: move umbrechen, da sie einen Namen haben. Std :: move weist den Compiler an, den Wert in eine r-Wert-Referenz umzuwandeln, unabhängig von den Parametern. Im technischen Sinne ist std :: move eine Analogie zu " std :: rvalue "

Nach dem Verschieben wird die Ressource des unique_ptr auf einen anderen unique_ptr übertragen

Es gibt viele Themen, die die Wertreferenz dokumentieren. Dies ist zunächst ziemlich einfach .

Bearbeiten:

Das bewegte Objekt bleibt gültig, aber nicht spezifiziert .

C ++ Primer 5, ch13 geben auch eine sehr gute Erklärung, wie das Objekt "verschoben" wird

StereoMatching
quelle
1
Was passiert also mit dem Objekt, anachdem std :: move (a) im bmove-Konstruktor aufgerufen wurde? Ist es einfach total ungültig?
David Doria
3

Ich schlage vor, make_unique zu verwenden

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;
}
Spritzen
quelle
-1

unique_ptr ist nicht kopierbar, es ist nur beweglich.

Dies wirkt sich direkt auf Test aus, der in Ihrem zweiten Beispiel ebenfalls nur beweglich und nicht kopierbar ist.

In der Tat ist es gut, dass Sie verwenden, unique_ptrwas Sie vor einem großen Fehler schützt.

Das Hauptproblem bei Ihrem ersten Code ist beispielsweise, dass der Zeiger niemals gelöscht wird, was wirklich sehr, sehr schlecht ist. Angenommen, Sie würden dies beheben durch:

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

Das ist auch schlecht. Was passiert, wenn Sie kopieren Test? Es gibt zwei Klassen mit einem Zeiger, der auf dieselbe Adresse zeigt.

Wenn einer Testzerstört wird, zerstört er auch den Zeiger. Wenn Ihre Sekunde Testzerstört wird, wird versucht, auch den Speicher hinter dem Zeiger zu entfernen. Es wurde jedoch bereits gelöscht und es wird ein fehlerhafter Laufzeitfehler beim Speicherzugriff angezeigt (oder ein undefiniertes Verhalten, wenn wir Pech haben).

Der richtige Weg ist also, entweder den Kopierkonstruktor und den Kopierzuweisungsoperator zu implementieren, damit das Verhalten klar ist und wir eine Kopie erstellen können.

unique_ptrist uns hier weit voraus. Es hat die semantische Bedeutung: " Ich bin es unique, also können Sie mich nicht einfach kopieren. " Es verhindert also den Fehler, die vorliegenden Operatoren jetzt zu implementieren.

Sie können den Kopierkonstruktor und den Kopierzuweisungsoperator für ein spezielles Verhalten definieren, und Ihr Code funktioniert. Aber Sie sind zu Recht (!) Dazu gezwungen.

Die Moral der Geschichte: Verwenden Sie immer unique_ptrin solchen Situationen.

Eisfeuer
quelle