logo
Contato | Sobre...        
rebarba rebarba

Rodrigo Strauss :: Blog


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... :-)
rebarba rebarba
  ::::