Copiar constructor para una clase con unique_ptr

105

¿Cómo implemento un constructor de copia para una clase que tiene una unique_ptrvariable miembro? Solo estoy considerando C ++ 11.

codefx
fuente
9
Bueno, ¿qué quieres que haga el constructor de copias?
Nicol Bolas
Leí que unique_ptr no se puede copiar. Esto me hace preguntarme cómo se usa una clase que tiene una variable miembro unique_ptr en un archivo std::vector.
codefx
2
@AbhijitKadam Puede hacer una copia profunda del contenido de unique_ptr. De hecho, eso suele ser lo más sensato.
Cubic
2
Tenga en cuenta que posiblemente esté haciendo la pregunta incorrecta. Probablemente no desee un constructor de copia para su clase que contenga a unique_ptr, probablemente desee un constructor de movimiento, si su objetivo es poner los datos en un std::vector. Por otro lado, el estándar C ++ 11 ha creado automáticamente constructores de movimiento, por lo que tal vez desee un constructor de copia ...
Yakk - Adam Nevraumont
3
Los elementos vectoriales @codefx no tienen que ser copiables; solo significa que el vector no se podrá copiar.
MM

Respuestas:

81

Dado unique_ptrque no se puede compartir, debe realizar una copia profunda de su contenido o convertirlo unique_ptren un archivo 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;
}

Puede, como mencionó NPE, usar un move-ctor en lugar de un copy-ctor, pero eso resultaría en una semántica diferente de su clase. Un move-ctor necesitaría hacer que el miembro se pueda mover explícitamente a través de std::move:

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

Tener un conjunto completo de los operadores necesarios también conduce 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,
}

Si desea utilizar su clase en a std::vector, básicamente debe decidir si el vector será el propietario único de un objeto, en cuyo caso sería suficiente para hacer que la clase sea movible, pero no copiable. Si omite copy-ctor y copy-assign, el compilador lo guiará sobre cómo usar un std :: vector con tipos de solo movimiento.

Daniel Frey
fuente
4
¿Vale la pena mencionar a los constructores de movimientos?
NPE
4
+1, pero el constructor de movimientos debe enfatizarse aún más. En un comentario, el OP dice que el objetivo es usar el objeto en un vector. Para eso, la construcción de mudanzas y la asignación de mudanzas son las únicas cosas necesarias.
jogojapan
36
Como advertencia, la estrategia anterior funciona para tipos simples como int. Si tiene un unique_ptr<Base>que almacena un Derived, lo anterior se dividirá.
Yakk - Adam Nevraumont
5
No hay verificación de nulo, por lo que tal como está, esto permite una desreferencia de nullptr. Qué talA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining
1
@Aaron en situaciones polimórficas, el borrador se borrará de algún modo, o será inútil (si conoce el tipo a borrar, ¿por qué cambiar solo el borrador?). En cualquier caso, sí, este es el diseño de un value_ptr...unique_ptr más Deleter / copiadora información.
Yakk - Adam Nevraumont
46

El caso habitual para que uno tenga un unique_ptren una clase es poder usar la herencia (de lo contrario, un objeto simple a menudo también lo haría, consulte RAII). Para este caso, no hay una respuesta adecuada en este hilo hasta ahora .

Entonces, aquí está el punto de partida:

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

... y el objetivo es, como se dijo, hacer Foocopiable.

Para esto, es necesario hacer una copia profunda del puntero contenido para garantizar que la clase derivada se copie correctamente.

Esto se puede lograr agregando el siguiente código:

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

Básicamente, están sucediendo dos cosas aquí:

  • El primero es la adición de constructores de copiar y mover, que se eliminan implícitamente en Foo el constructor de copia de unique_ptr. El constructor de movimiento se puede agregar simplemente por = default... que es solo para que el compilador sepa que el constructor de movimiento habitual no se eliminará (esto funciona, unique_ptrya que ya tiene un constructor de movimiento que se puede usar en este caso).

    Para el constructor de copia de Foo, no existe un mecanismo similar ya que no hay constructor de copia de unique_ptr. Entonces, uno tiene que construir uno nuevo unique_ptr, llenarlo con una copia del pointee original y usarlo como miembro de la clase copiada.

  • En caso de que se trate de una herencia, la copia del pointee original debe hacerse con cuidado. La razón es que hacer una copia simple a través std::unique_ptr<Base>(*ptr)del código anterior resultaría en un corte, es decir, solo se copia el componente base del objeto, mientras que falta la parte derivada.

    Para evitar esto, la copia debe realizarse mediante el patrón de clonación. La idea es hacer la copia a través de una función virtual clone_impl()que devuelve a Base*en la clase base. En la clase derivada, sin embargo, se extiende mediante covarianza para devolver a Derived*, y este puntero apunta a una copia recién creada de la clase derivada. La clase base puede acceder a este nuevo objeto a través del puntero de la clase base Base*, envolverlo en a unique_ptry devolverlo a través de la clone()función real que se llama desde el exterior.

Davidhigh
fuente
3
Esta debería haber sido la respuesta aceptada. Todos los demás van en círculos en este hilo, sin insinuar por qué uno desearía copiar un objeto señalado por unique_ptrcuando la contención directa haría lo contrario. ¿¿¿La respuesta??? Herencia .
Tanveer Badar
4
Uno puede estar usando unique_ptr incluso cuando conoce el tipo concreto al que se apunta por una variedad de razones: 1. Debe ser anulable. 2. El puntero es muy grande y es posible que tengamos un espacio de pila limitado. A menudo (1) y (2) se van de la mano, por lo tanto, uno podría en ocasiones prefieren unique_ptrmás optionalpara los tipos anulables.
Ponkadoodle
3
El idioma de las espinillas es otra razón.
emsr
¿Qué pasa si una clase base no debería ser abstracta? Dejarlo sin el especificador puro puede provocar errores en el tiempo de ejecución si olvida volver a implementarlo en derivado.
Oleksij Plotnyc'kyj
1
@ OleksijPlotnyc'kyj: sí, si implementas el clone_implin base, el compilador no te dirá si lo olvidas en la clase derivada. Sin embargo, podría usar otra clase base Cloneablee implementar un virtual puro clone_implallí. Entonces el compilador se quejará si lo olvida en la clase derivada.
Davidhigh
11

Pruebe este ayudante para crear copias profundas y afronte cuando la fuente unique_ptr sea nula.

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

P.ej:

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

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};
Scott Langham
fuente
2
¿Se copiará correctamente si la fuente apunta a algo derivado de T?
Roman Shapovalov
3
@RomanShapovalov No, probablemente no, te cortarían. En ese caso, la solución probablemente sería agregar un método virtual unique_ptr <T> clone () a su tipo T, y proporcionar anulaciones del método clone () en tipos derivados de T. El método clone crearía una nueva instancia de el tipo derivado y devolverlo.
Scott Langham
¿No hay punteros únicos / de ámbito en c ++ o bibliotecas boost que tengan incorporada la funcionalidad de copia profunda? Sería bueno no tener que crear nuestros constructores de copia personalizados, etc. para las clases que usan estos punteros inteligentes, cuando queremos el comportamiento de copia profunda, que suele ser el caso. Sólo me preguntaba.
shadow_map
5

Daniel Frey menciona sobre la solución de copia, yo hablaría sobre cómo mover el 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_;
};

Se llaman constructor de movimiento y asignación de movimiento.

podrías usarlos así

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

}

Necesita envolver ayc por std :: move porque tienen un nombre std :: move le está diciendo al compilador que transforme el valor en una referencia rvalue cualesquiera que sean los parámetros. En sentido técnico, std :: move es una analogía con algo como " std :: rvalue "

Después de mover, el recurso de unique_ptr se transfiere a otro unique_ptr

Hay muchos temas que documentan la referencia de rvalue; este es bastante fácil para empezar .

Editar:

El objeto movido seguirá siendo un estado válido pero no especificado .

C ++ primer 5, ch13 también da una muy buena explicación sobre cómo "mover" el objeto

StereoMatching
fuente
1
entonces, ¿qué sucede con el objeto adespués de llamar a std :: move (a) en el bconstructor de movimiento? ¿Es totalmente inválido?
David Doria
3

Sugiero usar 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;
}
Chapoteo
fuente
-1

unique_ptr no es copiable, solo es movible.

Esto afectará directamente a Test, que es, en su segundo ejemplo, también solo movible y no copiable.

De hecho, es bueno que uses lo unique_ptrque te protege de un gran error.

Por ejemplo, el problema principal con su primer código es que el puntero nunca se elimina, lo que es realmente malo. Diga, solucionaría esto de la siguiente manera:

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

Esto también es malo. ¿Qué pasa si copiaTest ? Habrá dos clases que tengan un puntero que apunte a la misma dirección.

Cuando uno Testse destruye, también destruirá el puntero. Cuando Testse destruye su segundo , también intentará eliminar la memoria detrás del puntero. Pero ya se ha eliminado y obtendremos algún error de tiempo de ejecución de acceso a la memoria incorrecto (o un comportamiento indefinido si no tenemos suerte).

Entonces, la forma correcta es implementar el constructor de copia y el operador de asignación de copia, de modo que el comportamiento sea claro y podamos crear una copia.

unique_ptrestá muy por delante de nosotros aquí. Tiene el significado semántico: " Yo soy unique, así que no puedes simplemente copiarme. Por lo tanto, nos evita el error de implementar ahora los operadores en cuestión.

Puede definir el constructor de copia y el operador de asignación de copia para un comportamiento especial y su código funcionará. Pero estás, con razón (!), Obligado a hacer eso.

La moraleja de la historia: utilizar siempre unique_ptren este tipo de situaciones.

Hielo fuego
fuente