Quando temos que usar construtores de cópia?

87

Eu sei que o compilador C ++ cria um construtor de cópia para uma classe. Nesse caso, temos que escrever um construtor de cópia definido pelo usuário? Voce pode dar alguns exemplos?

penguru
fonte
1
Um dos casos para escrever seu próprio copy-ctor: Quando você tem que fazer deep copy. Observe também que, assim que você cria um ctor, não há nenhum ctor padrão criado para você (a menos que você use a palavra-chave padrão).
harshvchawla

Respostas:

75

O construtor de cópia gerado pelo compilador faz a cópia por membro. Às vezes, isso não é suficiente. Por exemplo:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

neste caso, a cópia do storedmembro não duplicará o buffer (apenas o ponteiro será copiado), então a primeira cópia a ser destruída que compartilha o buffer será chamada com delete[]êxito e a segunda terá um comportamento indefinido. Você precisa do construtor de cópia profunda (e também do operador de atribuição).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}
dente afiado
fonte
10
Não executa bit a bit, mas cópia de membro que em particular invoca o copy-ctor para membros de tipo de classe.
Georg Fritzsche
7
Não escreva o operador de classificação assim. Não é uma exceção segura. (se o novo lançar uma exceção, o objeto é deixado em um estado indefinido com o armazenamento apontando para uma parte desalocada da memória (desalocar a memória SOMENTE depois que todas as operações que podem lançar tenham sido concluídas com sucesso)). Uma solução simples é usar o idium de troca de cópia.
Martin York
@sharptooth 3ª linha a partir do fundo você tem delete stored[];e eu acredito que deveria serdelete [] stored;
Peter Ajtai
4
Eu sei que é apenas um exemplo, mas você deve apontar que a melhor solução é usar std::string. A ideia geral é que apenas as classes de utilitários que gerenciam recursos precisam sobrecarregar os Três Grandes e que todas as outras classes devem apenas usar essas classes de utilitários, eliminando a necessidade de definir qualquer um dos Três Grandes.
GManNickG
2
@Martin: Queria ter certeza de que foi esculpido em pedra. : P
GManNickG
46

Estou um pouco irritado porque a regra do Rule of Fivenão foi citada.

Esta regra é muito simples:

A regra de cinco :
Sempre que você estiver escrevendo um dos Destruidor, Construtor de Cópia, Operador de Atribuição de Cópia, Construtor de Mover ou Operador de Atribuição de Mover, você provavelmente precisará escrever os outros quatro.

Mas há uma orientação mais geral que você deve seguir, que deriva da necessidade de escrever um código seguro para exceção:

Cada recurso deve ser gerenciado por um objeto dedicado

Aqui @sharptooth's código ainda é (principalmente) bem, no entanto, se ele fosse para adicionar um segundo atributo à sua classe que não seria. Considere a seguinte classe:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

O que acontece se new Barjogar? Como você exclui o objeto apontado por mFoo? Existem soluções (nível de função try / catch ...), eles simplesmente não escalam.

A maneira adequada de lidar com a situação é usar classes adequadas em vez de ponteiros brutos.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Com a mesma implementação de construtor (ou, na verdade, usando make_unique), agora tenho segurança de exceção de graça !!! Não é emocionante? E o melhor de tudo, não preciso mais me preocupar com um destruidor adequado! Eu preciso escrever meu próprioCopy Constructor e Assignment Operatorporqueunique_ptr não define essas operações ... mas não importa aqui;)

E, portanto, sharptootha aula de revisitada:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Eu não sei sobre você, mas acho o meu mais fácil;)

Matthieu M.
fonte
Para C ++ 11 - a regra de cinco que adiciona à regra de três o Construtor de Movimento e o Operador de Atribuição de Movimento.
Robert Andrzejuk
1
@Robb: Observe que, na verdade, conforme demonstrado no último exemplo, você deve geralmente buscar a Regra de Zero . Apenas classes técnicas especializadas (genéricas) devem se preocupar com o manuseio de um recurso, todas as outras classes devem usar esses ponteiros / contêineres inteligentes e não se preocupar com isso.
Matthieu M.
@MatthieuM. Concordo :-) Mencionei a Regra dos Cinco, já que esta resposta é anterior ao C ++ 11 e começa com "Três Grandes", mas deve ser mencionado que agora os "Cinco Grandes" são relevantes. Não quero votar contra esta resposta, pois está correta no contexto solicitado.
Robert Andrzejuk
@Robb: Bom ponto, eu atualizei a resposta para mencionar a regra de cinco em vez de três grandes. Espero que a maioria das pessoas tenha mudado para compiladores capazes de C ++ 11 agora (e eu tenho pena daqueles que ainda não o fizeram).
Matthieu M.
32

Posso me lembrar de minha prática e pensar nos seguintes casos, quando é preciso lidar com a declaração / definição explícita do construtor de cópia. Eu agrupei os casos em duas categorias

  • Correção / Semântica - se você não fornecer um construtor de cópia definido pelo usuário, os programas que usam esse tipo podem falhar na compilação ou podem funcionar incorretamente.
  • Otimização - fornecer uma boa alternativa para o construtor de cópia gerado pelo compilador permite tornar o programa mais rápido.


Correção / Semântica

Coloco nesta seção os casos em que declarar / definir o construtor de cópia é necessário para a operação correta dos programas que usam aquele tipo.

Depois de ler esta seção, você aprenderá sobre as várias armadilhas de permitir que o compilador gere o construtor de cópia por conta própria. Portanto, como Seand observou em sua resposta , é sempre seguro desativar a capacidade de cópia para uma nova classe e ativá-la deliberadamente mais tarde, quando realmente necessário.

Como tornar uma classe não copiável em C ++ 03

Declare um construtor de cópia privado e não forneça uma implementação para ele (de modo que a construção falhe no estágio de vinculação, mesmo se os objetos daquele tipo forem copiados no próprio escopo da classe ou por seus amigos).

Como tornar uma classe não copiável em C ++ 11 ou mais recente

Declare o construtor de cópia com =deleteno final.


Cópia superficial vs profunda

Este é o caso mais bem compreendido e, na verdade, o único mencionado nas outras respostas. shaprtooth tem coberto muito bem. Só quero acrescentar que os recursos de cópia profunda que deveriam pertencer exclusivamente ao objeto podem se aplicar a qualquer tipo de recurso, dos quais a memória alocada dinamicamente é apenas um tipo. Se necessário, copiar profundamente um objeto também pode exigir

  • copiando arquivos temporários no disco
  • abrindo uma conexão de rede separada
  • criando um thread de trabalho separado
  • alocar um framebuffer OpenGL separado
  • etc

Objetos de autorregistro

Considere uma classe onde todos os objetos - não importa como foram construídos - DEVEM ser registrados de alguma forma. Alguns exemplos:

  • O exemplo mais simples: manter a contagem total de objetos existentes atualmente. O registro de objetos trata apenas de incrementar o contador estático.

  • Um exemplo mais complexo é ter um registro singleton, onde as referências a todos os objetos existentes desse tipo são armazenadas (para que as notificações possam ser entregues a todos eles).

  • Os ponteiros inteligentes contados por referência podem ser considerados apenas um caso especial nesta categoria: o novo ponteiro "registra-se" com o recurso compartilhado em vez de em um registro global.

Essa operação de autorregistro deve ser executada por QUALQUER construtor do tipo e o construtor de cópia não é exceção.


Objetos com referências cruzadas internas

Alguns objetos podem ter uma estrutura interna não trivial com referências cruzadas diretas entre seus diferentes subobjetos (na verdade, apenas uma dessas referências cruzadas internas é suficiente para acionar este caso). O construtor de cópia fornecido pelo compilador quebrará as associações internas entre objetos , convertendo- as em associações entre objetos .

Um exemplo:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Apenas objetos que atendam a certos critérios podem ser copiados

Pode haver classes em que os objetos são seguros para copiar enquanto estão em algum estado (por exemplo, estado padrão construído) e não seguros para copiar de outra forma. Se quisermos permitir a cópia segura de objetos de cópia, então - se estiver programando defensivamente - precisamos de uma verificação em tempo de execução no construtor de cópia definido pelo usuário.


Subobjetos não copiáveis

Às vezes, uma classe que deveria ser copiável agrega subobjetos não copiáveis. Normalmente, isso acontece para objetos com estado não observável (esse caso é discutido em mais detalhes na seção "Otimização" abaixo). O compilador apenas ajuda a reconhecer esse caso.


Subobjetos quase copiáveis

Uma classe, que deve ser copiável, pode agregar um subobjeto de um tipo quase copiável. Um tipo quase copiável não fornece um construtor de cópia no sentido estrito, mas possui outro construtor que permite criar uma cópia conceitual do objeto. A razão para tornar um tipo quase copiável é quando não há acordo total sobre a semântica de cópia do tipo.

Por exemplo, revisitando o caso de autorregistro do objeto, podemos argumentar que pode haver situações em que um objeto deve ser registrado com o gerenciador de objeto global apenas se for um objeto autônomo completo. Se for um subobjeto de outro objeto, então a responsabilidade de gerenciá-lo é com o objeto que o contém.

Ou, tanto a cópia superficial quanto a profunda devem ser suportadas (nenhuma delas sendo o padrão).

Em seguida, a decisão final é deixada para os usuários desse tipo - ao copiar objetos, eles devem especificar explicitamente (por meio de argumentos adicionais) o método de cópia pretendido.

No caso de uma abordagem não defensiva da programação, também é possível que um construtor de cópia regular e um construtor de quase cópia estejam presentes. Isso pode ser justificado quando, na grande maioria dos casos, um único método de cópia deve ser aplicado, enquanto em situações raras, mas bem conhecidas, métodos de cópia alternativos devem ser usados. Então, o compilador não reclamará que é incapaz de definir implicitamente o construtor de cópia; será de responsabilidade exclusiva dos usuários lembrar e verificar se um subobjeto daquele tipo deve ser copiado por meio de um construtor de quase cópia.


Não copie o estado que está fortemente associado à identidade do objeto

Em casos raros, um subconjunto de objetos observáveis estado pode constituir (ou ser considerado) uma parte inseparável da identidade do objeto e não deve ser transferível para outros objetos (embora isso possa ser um tanto controverso).

Exemplos:

  • O UID do objeto (mas este também pertence ao caso de "autorregistro" de cima, já que o id deve ser obtido em um ato de autorregistro).

  • Histórico do objeto (por exemplo, a pilha Desfazer / Refazer) no caso em que o novo objeto não deve herdar o histórico do objeto de origem, mas em vez disso, começar com um único item de histórico " Copiado às <TIME> de <OTHER_OBJECT_ID> ".

Nesses casos, o construtor de cópia deve ignorar a cópia dos subobjetos correspondentes.


Obrigando a assinatura correta do construtor de cópia

A assinatura do construtor de cópia fornecido pelo compilador depende de quais construtores de cópia estão disponíveis para os subobjetos. Se pelo menos um subobjeto não tiver um construtor de cópia real (tomando o objeto de origem por referência constante), mas em vez disso tiver um construtor de cópia mutante (tomando o objeto de origem por referência não constante), o compilador não terá escolha mas para declarar implicitamente e então definir um construtor de cópia mutante.

Agora, o que aconteceria se o construtor de cópia "mutante" do tipo do subobjeto não alterasse realmente o objeto de origem (e fosse simplesmente escrito por um programador que não sabe sobre a constpalavra - chave)? Se não podemos ter esse código corrigido adicionando o que falta const, então a outra opção é declarar nosso próprio construtor de cópia definido pelo usuário com uma assinatura correta e cometer o pecado de mudar para um const_cast.


Cópia na gravação (COW)

Um contêiner COW que forneceu referências diretas aos seus dados internos DEVE ser copiado em profundidade no momento da construção, caso contrário, ele pode se comportar como um indicador de contagem de referência.

Embora COW seja uma técnica de otimização, essa lógica no construtor de cópia é crucial para sua implementação correta. É por isso que coloquei este caso aqui e não na seção "Otimização", para onde iremos a seguir.



Otimização

Nos casos a seguir, você pode querer / precisar definir seu próprio construtor de cópia por questões de otimização:


Otimização da estrutura durante a cópia

Considere um contêiner que suporte operações de remoção de elemento, mas pode fazer isso simplesmente marcando o elemento removido como excluído e reciclar seu slot mais tarde. Quando uma cópia desse contêiner é feita, pode fazer sentido compactar os dados remanescentes em vez de preservar os slots "excluídos" como estão.


Pular a cópia do estado não observável

Um objeto pode conter dados que não fazem parte de seu estado observável. Normalmente, são dados armazenados em cache / memoized acumulados durante a vida útil do objeto, a fim de acelerar certas operações lentas de consulta realizadas pelo objeto. É seguro pular a cópia desses dados, pois eles serão recalculados quando (e se!) As operações relevantes forem realizadas. Copiar esses dados pode ser injustificado, pois pode ser rapidamente invalidado se o estado observável do objeto (a partir do qual os dados armazenados em cache são derivados) for modificado por operações de mutação (e se não vamos modificar o objeto, por que estamos criando um profundo copia então?)

Essa otimização é justificada apenas se os dados auxiliares forem grandes em comparação com os dados que representam o estado observável.


Desativar cópia implícita

C ++ permite desabilitar a cópia implícita, declarando o construtor de cópia explicit. Então, os objetos daquela classe não podem ser passados ​​para funções e / ou retornados de funções por valor. Esse truque pode ser usado para um tipo que parece ser leve, mas é realmente muito caro para copiar (embora, torná-lo quase copiável pode ser uma escolha melhor).

Em C ++ 03, declarar um construtor de cópia exigia defini-lo também (é claro, se você pretendia usá-lo). Portanto, escolher esse construtor de cópia meramente por causa da preocupação que está sendo discutida significava que você tinha que escrever o mesmo código que o compilador geraria automaticamente para você.

C ++ 11 e padrões mais recentes permitem declarar funções de membro especiais (os construtores padrão e de cópia, o operador de atribuição de cópia e o destruidor) com uma solicitação explícita para usar a implementação padrão (apenas termine a declaração com =default).



TODOs

Essa resposta pode ser melhorada da seguinte maneira:

  • Adicione mais código de exemplo
  • Ilustrar o caso "Objetos com referências cruzadas internas"
  • Adicione alguns links
Leon
fonte
6

Se você tiver uma classe com conteúdo alocado dinamicamente. Por exemplo, você armazena o título de um livro como um caractere * e define o título com novo, a cópia não funcionará.

Você teria que escrever um construtor de cópia que faça title = new char[length+1]e então strcpy(title, titleIn). O construtor de cópia faria apenas uma cópia "superficial".

Peter Ajtai
fonte
2

O Construtor de cópia é chamado quando um objeto é passado por valor, retornado por valor ou copiado explicitamente. Se não houver um construtor de cópia, c ++ cria um construtor de cópia padrão que faz uma cópia superficial. Se o objeto não tiver ponteiros para memória alocada dinamicamente, uma cópia superficial servirá.

Josh
fonte
0

Geralmente é uma boa ideia desativar o copy ctor e o operator =, a menos que a classe precise especificamente deles. Isso pode evitar ineficiências, como passar um argumento por valor quando a referência é pretendida. Além disso, os métodos gerados pelo compilador podem ser inválidos.

Seand
fonte
-1

Vamos considerar o snippet de código abaixo:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();fornece saída de lixo porque há um construtor de cópia definido pelo usuário criado sem nenhum código escrito para copiar dados explicitamente. Portanto, o compilador não cria o mesmo.

Pensei apenas em compartilhar esse conhecimento com todos vocês, embora a maioria de vocês já saiba disso.

Saúde ... Boa codificação !!!

Pankaj Kumar Thapa
fonte