terça-feira, 13 de fevereiro de 2007

Dica C++: Classes sem encrencas

Acompanhe o seguinte código:

 

class Relogio
{
public:
    void Tick() const
    {
        const int UM_SEGUNDO = 1000; // ms
  
        Wait( UM_SEGUNDO );
    }; 
};


class BombaRelogio
{
public:
    BombaRelogio(const int contagemRegressiva)
    {
        _contagemRegressiva = contagemRegressiva;
        _relogio = new Relogio;
    }
 
    ~BombaRelogio()
    {
        delete _relogio;
    }
 
    void Ativar() const
    {
        int contador = _contagemRegressiva;
  
        while ( contador > 0 )
        {
            _relogio->Tick();
   
            contador--;
        }
  
        Boom();
    }
 
    void Boom() const
    { 
        cout << "Boom !" << endl;
    }
 
 
private:
    int _contagemRegressiva;
    Relogio *_relogio;
};

 

A princípio esta classe parece funcionar bem e não haver algum problema:

 

...
{
    BombaRelogio bomba( 10 );

    bomba.Ativar();
}

 

Mas por trás de uma declaração "inocente" como esta se escondem bugs traiçoeiros... ;).

Compiladores C++ foram programados para gerar código por default, quando não deixamos o explícito na classe.

No caso acima, por exemplo, um construtor padrão - sem parâmetros de inicialização - foi automaticamente gerado. Algo como isto:

 

BombaRelogio::BombaRelogio()
{
}

 

Assim, ao criarmos um objeto de BombaRelogio sem passar parâmetros, os atributos não são inicializados, causando erros:

 

...
{
    // Nao instancia _relogio
    // nem inicializa _contagemRegressiva.

    BombaRelogio bomba;

    // Erro de acesso invalido a memoria
    // (tentando acessar _relogio) e loop
    // com duracao arbitraria (por nao se
    // ter conhecimento do valor de
    // _contagemRegressiva.

    bomba.Ativar();

    // Ao destrutor ser executado (no fim
    // deste bloco de codigo) ele ainda
    // tentara' destruir um suposto objeto
    // instanciado em _relogio. Isso causara'
    // mais um erro, ja' que o objeto nao
    // havia sido instanciado.

}

 

Veja quantos erros em tão pouco código...

Uma declaração aparentemente inofensiva causando tantos problemas. Bom, além desses ainda podem haver outros.  O que, por exemplo, se espera de uma atribuição do tipo:

 

BombaRelogio bombaRapida( 5 );
BombaRelogio bombaLenta( 60 );

bombaRapida = bombaLenta;

 

O que, a princípio, se espera é que ao atribuirmos bombaLenta a bombaRapida, a última fique com o valor da contagem regressiva de bombaLenta, certo ?

Bem, isso se esperava...

Como não declaramos um operador de atribuição, o compilador novamente se encarregou de fazê-lo por nós:

 

BombaRelogio& operator = (const BombaRelogio &bomba)
{
    return ( *this );
}

 

E esse operador default não implementa a funcionalidade que desejamos. Assim, a atribuição dos dois objetos não surtirá o efeito que esperamos dela.

O que se pode fazer, então, para que esta classe funcione sem estes tantos problemas ?

Restrinja o que seu código pode fazer.

Ao declarar uma classe, se você quer que ela possa ser construída pelo construtor padrão, declare-o. Senão desabilite-o.

Se você quer que o operador de atribuição funcione da maneira desejada, implemente-o. Senão desabilite-o.

Em C++, para desabilitar um construtor, um operador ou mesmo um método (de ser acessado), basta declará-lo fora da sessão pública.

Lembrando: Se quiser a possibilidade do acesso pelas classes filhas, declare-o como protegido. Senão declare-o como privado.

 

class BombaRelogio
{
public:
    BombaRelogio(const int contagemRegressiva)
    {
        _contagemRegressiva = contagemRegressiva;
        _relogio  = new Relogio;
    }
 
    ~BombaRelogio()
    {
        delete _relogio;
    }
 
    //...

protected:

    BombaRelogio()
    {
        _contagemRegressiva = 0;
        _relogio = new Relogio;
    }
 
 
    BombaRelogio& operator = (
      const BombaRelogio &bomba)
    {
        _contagemRegressiva =
           bomba.ContagemRegressiva;
 
        return ( *this );
    } 
};

 

Declarado assim, como protegido, você impede que um objeto de BombaRelogio possa ser instanciado sem receber a contagem regressiva e também impede de fazer atribuições entre objetos. Caso queira, declare os métodos como públicos.

Com estas simples ações, diversos tipos de problemas podem ser evitados. Portanto, acostume-se a executá-las. ;)

Generalizando:

  • Sempre declare um construtor sem parâmetros (default).
  • Sempre declare um operador de atribuição.

Se você quiser que um ou todos eles não sejam acessados, declare-os fora da parte pública.

 

A declaração de um operador de atribuição público leva à possibilidade de acrescentar à classe um construtor de cópia. Se você pode atribuir os valores de um objeto para outro, que tal você poder construir um objeto que seja um clone de um existente ?

 

BombaRelogio(const BombaRelogio &bomba)
{
    _relogio = new Relogio;
    *this = bomba;
}

 

Declarando mais este simples construtor, você adiciona a possibilidade de clonar objetos existentes:

 

BombaRelogio bombaRapida( 10 );
BombaRelogio bombaLigeira( bombaRapida );

 

Para completar os "requisitos" para uma classe com menos chance de problemas, aproveito para acrescentar este:

Acostume-se a declarar os destrutores como sendo virtuais.

Um destrutor ser virtual (assim como um método) significa que sua implementação pode ser redefinida em uma classe filha.

Acontece que, se você tiver uma classe pai que possua um destrutor não virtual e esse destrutor desaloque alguns objetos da memória, a classe filha terá de fazê-lo denovo. Isso porque o destrutor da classe pai não será executado após o da filha. Isso pode causar muitos problemas, sobretudo por esquecimento ou desconhecimento interno do objeto pai (desconhecimento esse que é até desejável). Sem falar nos casos em que a classe filha não pode nem mesmo acessar esses objetos que precisam ser desalocados, porque não possui acesso aos mesmos - se por exemplo, eles forem privados.

Logo, para evitar esse tipo de problema, sempre declare destrutores como sendo virtuais. Melhor serem e não precisar do que o contrário.

 

Conclusão

  • Restrinja o que seu código pode fazer.
  • Sempre declare um construtor sem parâmetros (default), mesmo que não seja público.
  • Sempre declare um operador de atribuição, mesmo que não seja público. Se for público, declare também um construtor de cópia, que faça uso do mesmo.
  • Sempre declare destrutores como sendo virtuais.

Nenhum comentário: