logo
Contato | Sobre...        
rebarba rebarba

Rodrigo Strauss :: Blog

follow us in feedly

Por que o bug?

Nós tínhamos um bug, que acontecia quando inseríamos (push_back) um objeto de uma determinada classe dentro de um container STL. Quando líamos o valor da variável, ela não correspondia ao valor do objeto inserido no container, e a runtime do C++ gerava um assert dizendo que estávamos chamando delete para um objeto mais de uma vez.

O problema nesse caso, foi causado por algo que o C++ não costuma fazer: um código gerado pelo compilador, algo que não foi você que fez. Nesse caso, o copy constructor. O copy constructor é um construtor especial, que é chamado quando um objeto é copiado. Isso acontece quando você atribui um objeto a outro, retorna um objeto de uma função, ou passa um objeto para uma função como valor. Um trecho de código vale mais que (pi^10) palavras:

class X
{
public:
  int i;
};

X func(X obj)
{
  X localx;

  // mais uma cópia
  localx = obj;

  localx.i = obj.i;

  // oh, estamos copiando novamente!
  return localx;
}

int main()
{
  X x1, x2;
 
  x2.i = 10; 

  // copiando...
  x1 = func(x2);

  return 0;
}

Na função "func" do exemplo acima, existem 3 operações de cópia: uma quando passamos x2 como parâmetro, outra quando atribuimos o parâmetro obj a localx, e outra na hora de retornar localx. Nessas situações, o copy constructor é chamado para copiar o objeto em questão. Como no nosso exemplo não temos um copy constructor definido, o compilador gera um automaticamente. Olhe como fica a nossa classe X com um copy constructor, equivalente ao que é gerado pelo compilador:

class X
{
public:
  //
  // se definirmos um copy constructor, o compilador não gerará mais
  // o construtor default. Então vamos fazê-lo
  //
  X()
  {}

  //
  // copy constructor, que tem a sintaxe [tipo(const tipo& param)]
  // esse copy constructor é equivalente ao gerado pelo compilador
  //
  X(const x& v)
  {
    i = v.i;
  }
  int i;
};

Para nossa classe X, o copy constructor não gera problemas. Agora, vamos ver o copy constructor equivalente ao gerado pelo compilador para nossa classe com bug:

class CTest2
{
private:
  CTest1* m_pTest1;
public:
   ...  

  //
  // copy constructor equivalente ao gerado pelo compilador
  //
  CTest2(const CTest2& v)
  {
    m_pTest1 = v.m_pTest1;
  }

  ...

  ~CTest2()
  {
    delete m_pTest1;
  }
};

Note que m_pTest1 é a única variável membro de CTest2. Então a única coisa que é feita é copiar o valor dessa variável (que é um ponteiro). Note que - isso é importante - o construtor não é rodado no caso de cópia de objeto. Sendo assim, o objeto cópia não terá um ponteiro alocado com new, mas sim, a cópia do ponteiro do objeto do qual ele foi copiado. Assim, tentaremos chamar delete para o mesmo ponteiro, mas nas duas instâncias de CTest2 - o que gera o assert que falei.

Se você está se perguntando onde é feita a cópia no código do exemplo do bug, repare que eu criei um objeto temporário diretamente ao invés de criar um objeto:
  //
  // criamos um objeto temporário do tipo CTest2, chamando
  // o construtor para inicializá-lo
  //
  vecTest2.push_back(CTest2(100, "1bit"));

O construtor do nosso objeto temporário é executado logo antes da chamada da função (push_back), e o destrutor é chamado logo após o retorno da função. A função push_back espera uma referência para o objeto (const CTest2&), então não é feita a cópia durante a passagem de parâmetros. Mas o objeto é copiado ao ser inserido no vector<>, o que faz com que o copy constructor gerado seja chamado, e copie o valor do ponteiro.

Uma das possíveis soluções é criarmos um copy constructor para nossa classe com bug, fazendo com que um novo objeto CTest1 seja criado durante a cópia. Assim, cada classe pode chamar delete para o seu ponteiro. Nossa classe ficaria assim:

class CTest2
{
private:
  CTest1* m_pTest1;
public:
   ...  

  CTest2(const CTest2& v)
  {
    m_pTest1 = new CTest1();


    //
    // por falar em copy constructor, essa instrução chamará o copy constructor
    // da classe CTest1, copiando todos os membros
    //
    *m_pTest1 = v.m_pTest1;
  }

  ...

  ~CTest2()
  {
    delete m_pTest1;
  }
};

Nossa solução é eficaz nesse caso. Mas e se precisássemos que as duas cópias usassem o mesmo ponteiro? Aguarde os próximos posts.


Em 09/04/2005 17:43, por Rodrigo Strauss


  
 
 
Comentários
Gomes | em 09/04/2005 | #
Cara, que coincidência, eu tava tava dando uma olhada em um problema no copy construtor agora pouco e postei no MSDN forum.

Fora o copy construtor tem também o conversion construtor que também faz coisas implícitas.
Minha mãe esqueceu de me dar um nome | em 11/04/2005 | #
Quando se tem ponteiros como membro da classe uma prática de segurança é implementar ou bloquear o contrutor de cópia e o operador de atribuição.

Exemplo:
class X
{
const X& operator=(const X& src);
X(const X&);

public:
// . . .
}
Desta forma X não vai linkar caso esteja usando algo que não foi implementado.
Thiago Adams | website | em 11/04/2005 | #
Esqueci de escrever meu nome de novo :)
Rodrigo Strauss | website | em 11/04/2005 | #
Eu vou colocar uma verificação para obrigar a colocar um nome... :-)
Algo a dizer?
Nome:


Site:


E-mail:


Escreva o número vinte e seis:


 Não mostre meu e-mail no site, não serve pra nada mesmo...

Comentário





Os comentários devem ser sobre assuntos relativos ao post, eu provavelmente apagarei comentários totalmente offtopic. Se quiser me enviar uma mensagem, use o formulário de contato. E não esqueça: isso é um site pessoal e eu me reservo o direito de apagar qualquer comentário ofensivo ou inapropriado.
rebarba rebarba
  ::::