Copier le constructeur d'une classe avec unique_ptr

105

Comment implémenter un constructeur de copie pour une classe qui a une unique_ptrvariable membre? Je ne considère que C ++ 11.

codefx
la source
9
Eh bien, que voulez-vous que le constructeur de copie fasse?
Nicol Bolas
J'ai lu que unique_ptr n'est pas copiable. Cela me fait me demander comment utiliser une classe qui a une variable membre unique_ptr dans un fichier std::vector.
codefx
2
@AbhijitKadam Vous pouvez faire une copie complète du contenu du fichier unique_ptr. En fait, c'est souvent la chose sensée à faire.
cubique
2
Veuillez noter que vous posez peut-être la mauvaise question. Vous ne voulez probablement pas d'un constructeur de copie pour votre classe contenant a unique_ptr, vous voulez probablement un constructeur de déplacement, si votre objectif est de placer les données dans un fichier std::vector. D'un autre côté, le standard C ++ 11 a créé automatiquement des constructeurs de déplacement, alors peut-être que vous voulez un constructeur de copie ...
Yakk - Adam Nevraumont
3
Les éléments vectoriels @codefx n'ont pas besoin d'être copiables; cela signifie simplement que le vecteur ne sera pas copiable.
MM

Réponses:

81

Étant donné que le unique_ptrne peut pas être partagé, vous devez soit profondément copier son contenu ou convertir unique_ptrà un 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;
}

Vous pouvez, comme NPE l'a mentionné, utiliser un move-ctor au lieu d'un copy-ctor, mais cela entraînerait une sémantique différente de votre classe. Un move-ctor devrait rendre le membre déplaçable explicitement via std::move:

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

Disposer d'un ensemble complet d'opérateurs nécessaires conduit également à

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

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

Si vous voulez utiliser votre classe dans a std::vector, vous devez essentiellement décider si le vecteur sera le propriétaire unique d'un objet, auquel cas il suffirait de rendre la classe déplaçable, mais pas copiable. Si vous omettez le copy-ctor et le copy-assignment, le compilateur vous guidera sur la façon d'utiliser un std :: vector avec des types move-only.

Daniel Frey
la source
4
Cela vaut-il la peine de mentionner les constructeurs de mouvements?
NPE
4
+1, mais le constructeur de mouvement devrait être encore plus souligné. Dans un commentaire, l'OP dit que le but est d'utiliser l'objet dans un vecteur. Pour cela, la construction du déménagement et l'affectation du déménagement sont les seules choses nécessaires.
jogojapan
36
En guise d'avertissement, la stratégie ci-dessus fonctionne pour des types simples comme int. Si vous avez un unique_ptr<Base>qui stocke un Derived, ce qui précède sera coupé.
Yakk - Adam Nevraumont
5
Il n'y a pas de vérification pour null, donc tel quel cela permet une déréférence nullptr. Que diriez-vousA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining
1
@Aaron dans les situations polymorphes, le suppresseur sera de type effacé en quelque sorte, ou inutile (si vous connaissez le type à supprimer, pourquoi ne changer que le suppresseur?). Dans tous les cas, oui, c'est la conception d'un value_ptr- unique_ptrplus d'informations de suppression / copieur.
Yakk - Adam Nevraumont
47

Le cas habituel pour avoir un unique_ptrdans une classe est de pouvoir utiliser l'héritage (sinon un objet simple ferait souvent l'affaire, voir RAII). Pour ce cas, il n'y a pas de réponse appropriée dans ce fil jusqu'à présent .

Alors, voici le point de départ:

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

... et le but est, comme dit, de rendre Foocopiable.

Pour cela, il faut faire une copie complète du pointeur contenu pour s'assurer que la classe dérivée est copiée correctement.

Cela peut être accompli en ajoutant le code suivant:

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

Il se passe essentiellement deux choses ici:

  • Le premier est l'ajout de constructeurs de copie et de déplacement, qui sont implicitement supprimés dans Foolorsque le constructeur de copie de unique_ptrest supprimé. Le constructeur de déplacement peut être ajouté simplement par = default... ce qui est juste pour indiquer au compilateur que le constructeur de déplacement habituel ne doit pas être supprimé (cela fonctionne, comme a unique_ptrdéjà un constructeur de déplacement qui peut être utilisé dans ce cas).

    Pour le constructeur de copie de Foo, il n'existe pas de mécanisme similaire car il n'y a pas de constructeur de copie de unique_ptr. Il faut donc en construire un nouveau unique_ptr, le remplir avec une copie de la pointee d'origine et l'utiliser comme membre de la classe copiée.

  • En cas d'héritage, la copie de la pointee originale doit être faite avec soin. La raison en est que faire une simple copie via std::unique_ptr<Base>(*ptr)dans le code ci-dessus entraînerait un découpage, c'est-à-dire que seul le composant de base de l'objet est copié, tandis que la partie dérivée est manquante.

    Pour éviter cela, la copie doit être effectuée via le clone-pattern. L'idée est de faire la copie via une fonction virtuelle clone_impl()qui retourne a Base*dans la classe de base. Dans la classe dérivée, cependant, il est étendu via la covariance pour renvoyer a Derived*, et ce pointeur pointe vers une copie nouvellement créée de la classe dérivée. La classe de base peut alors accéder à ce nouvel objet via le pointeur de classe de base Base*, l'envelopper dans un unique_ptr, et le renvoyer via la clone()fonction réelle qui est appelée de l'extérieur.

Davidhigh
la source
3
Cela aurait dû être la réponse acceptée. Tout le monde tourne en rond dans ce fil, sans indiquer pourquoi on souhaiterait copier un objet pointé par unique_ptralors que le confinement direct ferait autrement. La réponse??? L'héritage .
Tanveer Badar
4
On peut utiliser unique_ptr même lorsqu'ils connaissent le type concret pointé pour diverses raisons: 1. Il doit être nullable. 2. La pointee est très grande et nous pourrions avoir un espace de pile limité. Souvent (1) et (2) iront ensemble, donc on pourrait à l' occasion préfèrent unique_ptrplus optionalpour les types nullable.
Ponkadoodle
3
L'idiome des boutons est une autre raison.
emsr
Et si une classe de base ne devait pas être abstraite? Le laisser sans spécificateur pur peut conduire à des bogues d'exécution si vous oubliez de le réimplémenter dans un fichier dérivé.
olek stolar
1
@ OleksijPlotnyc'kyj: oui, si vous implémentez la clone_implbase in, le compilateur ne vous dira pas si vous l'oubliez dans la classe dérivée. Vous pouvez cependant utiliser une autre classe de base Cloneableet y implémenter un pur virtuel clone_impl. Ensuite, le compilateur se plaindra si vous l'oubliez dans la classe dérivée.
davidhigh le
11

Essayez cet assistant pour créer des copies complètes et faites face lorsque la source unique_ptr est nulle.

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

Par exemple:

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

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};
Scott Langham
la source
2
Copiera-t-il correctement si la source pointe vers quelque chose dérivé de T?
Roman Shapovalov
3
@RomanShapovalov Non, probablement pas, vous obtiendrez des tranches. Dans ce cas, la solution serait probablement d'ajouter une méthode virtuelle unique_ptr <T> clone () à votre type T, et de fournir des substitutions de la méthode clone () dans les types dérivés de T.La méthode clone créerait une nouvelle instance de le type dérivé et le renvoyer.
Scott Langham
N'existe-t-il pas de pointeurs uniques / étendus dans les bibliothèques C ++ ou Boost avec la fonctionnalité de copie approfondie intégrée? Ce serait bien de ne pas avoir à créer nos constructeurs de copie personnalisés, etc. pour les classes qui utilisent ces pointeurs intelligents, lorsque nous voulons le comportement de copie profonde, ce qui est souvent le cas. Je me demandais juste.
shadow_map
5

Daniel Frey mentionne la solution de copie, je parlerais de la façon de déplacer l'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_;
};

Ils sont appelés constructeur de déplacement et affectation de déplacement

tu pourrais les utiliser comme ça

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

}

Vous devez envelopper a et c par std :: move car ils ont un nom std :: move indique au compilateur de transformer la valeur en rvalue reference quels que soient les paramètres Au sens technique, std :: move est une analogie avec quelque chose comme " std :: rvalue "

Après le déplacement, la ressource de l'unique_ptr est transférée vers un autre unique_ptr

Il existe de nombreux sujets qui documentent la référence rvalue; c'est assez facile pour commencer .

Éditer :

L'objet déplacé doit rester dans un état valide mais non spécifié .

C ++ primer 5, ch13 donne également une très bonne explication sur la façon de "déplacer" l'objet

StereoMatching
la source
1
alors qu'arrive-t-il à l'objet aaprès avoir appelé std :: move (a) dans le bconstructeur de mouvement? Est-ce simplement totalement invalide?
David Doria
3

Je suggère d'utiliser 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;
}
Éclaboussure
la source
-1

unique_ptr n'est pas copiable, il est uniquement déplaçable.

Cela affectera directement Test, qui est, dans votre deuxième exemple, également uniquement mobile et non copiable.

En fait, il est bon que vous utilisiez unique_ptrce qui vous protège d'une grosse erreur.

Par exemple, le principal problème avec votre premier code est que le pointeur n'est jamais supprimé, ce qui est vraiment très mauvais. Dites, vous régleriez cela en:

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

C'est également mauvais. Que se passe-t-il si vous copiez Test? Il y aura deux classes qui ont un pointeur qui pointe vers la même adresse.

Quand on Testest détruit, cela détruira également le pointeur. Lorsque votre deuxième Testest détruit, il essaiera également de supprimer la mémoire derrière le pointeur. Mais il a déjà été supprimé et nous obtiendrons une mauvaise erreur d'exécution d'accès à la mémoire (ou un comportement indéfini si nous ne sommes pas chanceux).

Donc, la bonne façon est d'implémenter le constructeur de copie et l'opérateur d'affectation de copie, de sorte que le comportement soit clair et que nous puissions créer une copie.

unique_ptrest bien en avance sur nous ici. Il a la signification sémantique: " Je suis unique, donc vous ne pouvez pas simplement me copier. " Donc, cela nous évite de l'erreur d'implémenter maintenant les opérateurs à portée de main.

Vous pouvez définir le constructeur de copie et l'opérateur d'affectation de copie pour un comportement spécial et votre code fonctionnera. Mais vous êtes, à juste titre (!), Obligé de le faire.

La morale de l'histoire: toujours utiliser unique_ptrdans ce genre de situations.

Feu de glace
la source