Copie o construtor para uma classe com unique_ptr

105

Como faço para implementar um construtor de cópia para uma classe que possui uma unique_ptrvariável de membro? Estou considerando apenas o C ++ 11.

codefx
fonte
9
Bem, o que você deseja que o construtor de cópia faça?
Nicol Bolas,
Eu li que unique_ptr não pode ser copiado. Isso me faz pensar como usar uma classe que possui uma variável de membro unique_ptr em a std::vector.
codefx
2
@AbhijitKadam Você pode fazer uma cópia detalhada do conteúdo do unique_ptr. Na verdade, muitas vezes essa é a coisa sensata a fazer.
Cubic de
2
Observe que possivelmente você está fazendo a pergunta errada. Você provavelmente não deseja um construtor de cópia para sua classe contendo a unique_ptr, provavelmente deseja um construtor de movimentação, se seu objetivo for colocar os dados em a std::vector. Por outro lado, o padrão C ++ 11 criou automaticamente construtores de movimento, então talvez você queira um construtor de cópia ...
Yakk - Adam Nevraumont
3
Os elementos do vetor @codefx não precisam ser copiáveis; significa apenas que o vetor não poderá ser copiado.
MM

Respostas:

81

Uma vez que o unique_ptrnão pode ser compartilhado, você precisa copiar profundamente seu conteúdo ou convertê-lo unique_ptrem um 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;
}

Você pode, como o NPE mencionou, usar um move-ctor em vez de um copy-ctor, mas isso resultaria em semânticas diferentes de sua classe. Um move-ctor precisaria fazer o membro como móvel explicitamente via std::move:

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

Ter um conjunto completo de operadores necessários também leva 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 quiser usar sua classe em a std::vector, você basicamente terá que decidir se o vetor deve ser o único proprietário de um objeto, caso em que seria suficiente para tornar a classe móvel, mas não copiável. Se você deixar de fora o copy-ctor e a atribuição de cópia, o compilador irá guiá-lo sobre como usar um std :: vector com tipos move-only.

Daniel Frey
fonte
4
Pode valer a pena mencionar os construtores de movimento?
NPE de
4
+1, mas o construtor de movimento deve ser enfatizado ainda mais. Em um comentário, o OP diz que o objetivo é usar o objeto em um vetor. Para isso, a construção do movimento e a atribuição do movimento são as únicas coisas necessárias.
jogojapan de
36
Como um aviso, a estratégia acima funciona para tipos simples como int. Se você tiver um unique_ptr<Base>que armazene um Derived, o acima será dividido.
Yakk - Adam Nevraumont
5
Não há verificação de nulo, portanto, como está, isso permite uma desreferência de nullptr. Que talA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining
1
@Aaron em situações polimórficas, o deleter será apagado de alguma forma, ou sem sentido (se você sabe o tipo a deletar, por que mudar apenas o deleter?). Em qualquer caso, sim, este é o design de um value_ptr- unique_ptrmais informações do apagador / copiador.
Yakk - Adam Nevraumont
47

O caso usual de alguém ter um unique_ptrem uma classe é ser capaz de usar herança (caso contrário, um objeto simples faria o mesmo, consulte RAII). Para este caso, não há uma resposta apropriada neste tópico até agora .

Então, aqui está o ponto 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
};

... e o objetivo é, como disse, tornar Foocopiável.

Para isso, é necessário fazer uma cópia detalhada do ponteiro contido para garantir que a classe derivada seja copiada corretamente.

Isso pode ser feito adicionando o seguinte 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;
};

Existem basicamente duas coisas acontecendo aqui:

  • A primeira é a adição de construtores de cópia e movimentação, que são excluídos implicitamente à Foomedida que o construtor de cópia de unique_ptré excluído. O construtor de movimento pode ser adicionado simplesmente por = default... o que é apenas para deixar o compilador saber que o construtor de movimento usual não deve ser excluído (isso funciona, pois unique_ptrjá tem um construtor de movimento que pode ser usado neste caso).

    Para o construtor de cópia de Foo, não há mecanismo semelhante, pois não há construtor de cópia de unique_ptr. Portanto, é preciso construir um novo unique_ptr, preenchê-lo com uma cópia da ponta original e usá-lo como membro da classe copiada.

  • No caso de herança estar envolvida, a cópia da ponta original deve ser feita com cuidado. A razão é que fazer uma cópia simples via std::unique_ptr<Base>(*ptr)código acima resultaria em fracionamento, ou seja, apenas o componente base do objeto é copiado, enquanto a parte derivada está faltando.

    Para evitar isso, a cópia deve ser feita por meio do padrão de clone. A ideia é fazer a cópia por meio de uma função virtual clone_impl()que retorna um Base*na classe base. Na classe derivada, no entanto, ele é estendido por meio de covariância para retornar a Derived*, e esse ponteiro aponta para uma cópia recém-criada da classe derivada. A classe base pode então acessar esse novo objeto por meio do ponteiro da classe base Base*, envolvê-lo em um unique_ptre retorná-lo por meio da clone()função real que é chamada de fora.

davidhigh
fonte
3
Esta deveria ter sido a resposta aceita. Todos os outros estão andando em círculos neste tópico, sem sugerir por que alguém desejaria copiar um objeto apontado por unique_ptrquando a contenção direta faria o contrário. A resposta??? Herança .
Tanveer Badar
4
Alguém pode estar usando unique_ptr mesmo quando conhece o tipo concreto sendo apontado por uma variedade de razões: 1. Ele precisa ser anulável. 2. A ponta é muito grande e podemos ter um espaço de pilha limitado. Muitas vezes (1) e (2) vão juntos, portanto, um poder na ocasião preferem unique_ptrmais optionalpara tipos anuláveis.
Ponkadoodle
3
O idioma pimple é outro motivo.
emsr
E se uma classe base não for abstrata? Deixá-lo sem o especificador puro pode levar a erros de tempo de execução se você esquecer de reimplementá-lo no derivado.
Oleksij Plotnyc'kyj
1
@ OleksijPlotnyc'kyj: sim, se você implementar o clone_implin base, o compilador não dirá se você o esqueceu na classe derivada. Você poderia, no entanto, usar outra classe base Cloneablee implementar um virtual puro clone_impllá. Então o compilador reclamará se você esquecer na classe derivada.
Davidhigh
11

Experimente este auxiliar para criar cópias profundas e lidar com a origem de unique_ptr nulo.

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

Por exemplo:

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

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};
Scott Langham
fonte
2
Ele copiará corretamente se a fonte apontar para algo derivado de T?
Roman Shapovalov
3
@RomanShapovalov Não, provavelmente não, você cortaria. Nesse caso, a solução provavelmente seria adicionar um método virtual unique_ptr <T> clone () ao seu tipo T e fornecer substituições do método clone () em tipos derivados de T. O método clone faria uma nova instância de o tipo derivado e retornar isso.
Scott Langham
Não há ponteiros exclusivos / com escopo em c ++ ou bibliotecas de impulso que tenham a funcionalidade de cópia profunda embutida? Seria bom não ter que criar nossos construtores de cópia personalizados, etc. para classes que usam esses ponteiros inteligentes, quando queremos o comportamento de cópia profunda, o que geralmente é o caso. Apenas me perguntando.
shadow_map
5

Daniel Frey mencionou sobre a solução de cópia, gostaria de falar sobre como mover o 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_;
};

Eles são chamados de construtor de movimento e atribuição de movimento

você poderia usá-los assim

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

}

Você precisa envolver aec por std :: move porque eles têm um nome std :: move está dizendo ao compilador para transformar o valor em referência de rvalue quaisquer que sejam os parâmetros. Em sentido técnico, std :: move é uma analogia com algo como " std :: rvalue "

Depois de mover, o recurso do unique_ptr é transferido para outro unique_ptr

Existem muitos tópicos que documentam a referência de rvalue; este é muito fácil para começar .

Editar:

O objeto movido deve permanecer válido, mas em estado não especificado .

C ++ primer 5, ch13 também dá uma boa explicação sobre como "mover" o objeto

StereoMatching
fonte
1
então o que acontece com o objeto adepois de chamar std :: move (a) no bconstrutor de movimento? É totalmente inválido?
David Doria
3

Eu sugiro 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;
}
Splash
fonte
-1

unique_ptr não é copiável, é apenas móvel.

Isso afetará diretamente o Teste, que é, em seu segundo exemplo, também apenas móvel e não copiável.

Na verdade, é bom que você use o unique_ptrque o protege de um grande erro.

Por exemplo, o principal problema com seu primeiro código é que o ponteiro nunca é excluído, o que é muito, muito ruim. Diga, você consertaria isso:

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

Isso também é ruim. O que acontece, se você copiar Test? Haverá duas classes com um ponteiro que aponta para o mesmo endereço.

Quando um Testé destruído, ele também destrói o ponteiro. Quando o segundo Testfor destruído, ele tentará remover a memória atrás do ponteiro também. Mas ele já foi excluído e obteremos algum erro de runtime de acesso à memória ruim (ou comportamento indefinido se não tivermos sorte).

Portanto, a maneira certa é implementar o construtor de cópia e o operador de atribuição de cópia, para que o comportamento seja claro e possamos criar uma cópia.

unique_ptrestá muito à nossa frente aqui. Ele tem o significado semântico: “ Eu sou unique, então você não pode simplesmente me copiar. ” Assim, nos impede de cometer o erro de implementar agora os operadores em questão.

Você pode definir o construtor de cópia e o operador de atribuição de cópia para um comportamento especial e seu código funcionará. Mas você é, com razão (!), Forçado a fazer isso.

Moral da história: use sempre unique_ptrnesse tipo de situação.

Gelo fogo
fonte